I am trying to get a value from a swift dictionary to display as cell texts. I am getting error:
Type 'Any' has no subscript members
cell.audioLabel.text = audiofiles["filetitle"] <-line producing error
I believe I may have set the variables incorrectly, the value is being passed using segue from another tableview using the didSelectRowAt.
var audios = Array<Any>() <- gets from another controller
This is my current viewcontroller code:
import UIKit
class DetailViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
#IBOutlet weak var audioTable: UITableView!
#IBOutlet weak var descText: UITextView!
#IBOutlet weak var clickButton: UIButton!
var practitle = String()
var desc = String()
var hasAudio = Int()
var audios = Array<Any>()
override func viewDidLoad() {
super.viewDidLoad()
self.navigationItem.title = practitle
descText.text = desc
if hasAudio != 0 {
clickButton.isHidden = false
}
print(audios)
}
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return audios.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "audiocell", for: indexPath) as! DetailViewCell
let audiofiles = audios[indexPath.row]
cell.audioLabel.text = audiofiles["filetitle"]
return cell
}
When I use
print(audios)
The result that I get is:
[demoApp.Audiofile(id: 1, filetitle: "Sample file one", filename: "breath5mins", fileformat: "mp3"), demoApp.Audiofile(id: 2, filetitle: "Sample file two", filename: "breath10mins", fileformat: "mp3"), demoApp.Audiofile(id: 3, filetitle: "Sample file three", filename: "breath20mins", fileformat: "mp3")]
How can I use the filetitle as the label texts for my cell?
The goal is to display the title and open another view on cell click and allow the user to click a button on the new view and play the mp3 file.
Apparently the object is not a Swift dictionary, it's a custom class or struct.
You are fighting Swift's strong type system. Don't do that.
Declare audios with the static type
var audios = Array<AudioFile>()
and use dot notation instead of insecure KVC (which does not work anyway in this case).
cell.audioLabel.text = audiofiles.filetitle
You have declared your array as Array<Any> - which means that the array can contain anything; the elements of the array don't even all need to be the same type. As a result, the Swift compiler doesn't know what type of thing it is getting from audios[indexPath.row]; it could be a dictionary, it could be an array or it could be an integer. When you try and use subscripting, the compiler gives you an error because it doesn't know whether the item supports subscripting - i.e. an Int doesn't, and Any could be an Int.
Don't use Any or AnyObject in Swift if you know the actual type; Swift's type safety allows it to eliminate a large number of potential runtime issues at compile time by know what types things are.
From the print statement it appears that your array contains Audiofile instances (presumably a struct or class you have defined, not a dictionary). You should, therefore, declare audios correctly:
var audios = Array<Audiofile>()
You can then access the object's properties in your cellForRow:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "audiocell", for: indexPath) as! DetailViewCell
let audiofiles = audios[indexPath.row]
cell.audioLabel.text = audiofiles.filetitle
return cell
}
The reason you get the error is Any is an object with no definition. You can cast it into an AudioFile like this:
let audiofiles = audios[indexPath.row] as! AudioFile
If audios is an array of AudioFile only then declare it as such and you can use it the way you described.
Related
The piece of code below prints the content of whichever cell is clicked on in my TableView.
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
print(self.cell[indexPath.row])
}
I want to use the result that is printed in a label on another ViewController.
How do I get the string value from the function and then use it on on the other view? My thought is to use a global variable but I need to get the string value out first.
For example, You can use simple organization of a singleton of another ViewController (SecondScreen) with var main (in case, as usual, when SecondScreen inited via a Storyboard):
class SecondScreen : UIViewController {
// 1. add this var
static var main : SecondScreen? = nil
// 2. Your some UI element
#IBOutlet weak var textButton: UIButton!
// 3. add this method
func updateUI(string : String) {
textButton.setTitle(string, for: .normal)
}
// 4. setting a var
override func viewDidLoad() {
if SecondScreen.main == nil {
SecondScreen.main = self
}
}
// ... another your and standard methods
}
And you can update your SecondScreen like this:
let v = SecondScreen.main
v?.updateUI(string: "yourString")
Also I recommend you to call method async:
DispatchQueue.main.async {
SecondScreen.main?.updateUI(withString : string)
}
I suggest you to learn more about singletons...
At first, when you create a tableView, you have to collect data (string here) of cells in an array or another data collection. And you can get a needed data (strings) with indexPath variable in the method didSelectRowAt. And you can pass the string to another ViewController (let use SecondViewController) with several ways.
Here is an example:
// declaration an array of your strings
var array : [String] = ["First", "Second", "Third", ...]
...
// getting a string from method:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let string = array[indexPath.row]
print(string)
// next, for example, you need to pass the string to a singleton SecondViewController with static var **main**:
SecondViewController.main?.neededString = string
}
Don't forget update in async DispatchQueue:
DispatchQueue.main.async {
SecondViewController.main?.updateUI(withString : string)
}
Recently in my app I have been using Firebase to store information for my app and it has worked well. Now I am using it to stream videos with a web view being used in the tableview to display Youtube videos. When trying to link the WebView to the database, I get an error that says:
Type 'video' has no subscript members
What would be causing this?
Here is the code:
import UIKit
import Firebase
class videoController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var ref = DatabaseReference()
var video = [UIWebView]()
var databaseHandle:DatabaseHandle = 0
#IBOutlet var videoController: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
ref = Database.database().reference()
databaseHandle = ref.child("Videos").observe(.childAdded) { (snapshot) in
let post = snapshot.value as? UIWebView
if let actualPost = post {
self.video.append(actualPost)
self.videoController.reloadData()
}
}
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return video.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
video.videos.allowsInlineMediaPlayback = video[indexPath.row]
return(video)
}
}
This line:
let video = tableView.dequeueReusableCell(withIdentifier: "video") as! video
is your problem. This creates a new, local variable named video and it hides your video array property. Change it to:
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
Here's the whole method:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let videoCell = tableView.dequeueReusableCell(withIdentifier: "video") as! video
videoCell.videos.allowsInlineMediaPlayback = video[indexPath.row]
return videoCell
}
But on top of all of that, why do you have an array of web views? You certainly are not getting web views from Firebase.
And please fix your naming conventions. Class, struct, and enum names should start with uppercase letters. Variable, function, and case names start with lowercase letters. And use descriptive names. Naming everything simply video is confusing.
And change your video array to videos.
Before anyone suggests to pull the Firebase data from within the PlayerController's viewWillAppear, I already know how to do that and if I did it that way I know how to pass the data to the ScoreController. In this situation I need to pull the data directly from within the cell and somehow pass the data back from there.
I have a tableView inside a PlayerController that displays the randomPower, name, and score of each player. Inside the tableView's cell I pull the name and score from Firebase using a function getScoreDataFromFirebase(). The function is located directly inside the tableView's PlayerCell and once I get the values from Firebase I initialize the cell's name and score outlets right then and there.
Inside the tableView's cellForRowAtIndexPath I call cell.getScoreDataFromFirebase() and everything works fine because both outlets display the correct values.
From that point on I have a ScoreController. When a tableView cell is chosen the score is sent to the ScoreController.
The problem is since I'm pulling the data directly from within the cell itself the only way I could pass the score (pulled from Firebase) to ScoreController was to 1st set a didSet score property inside the cell.
Still inside the cell when I pull the score data from Firebase 2nd I initialize the score property with it
3rd inside the tableView's cellForAtIndexPath I use an if let to pass the value from the cell's score property to the the tableData.
When I first try to send the indexPath of that tableData over to the ScoreController sometimes it's nil even though the cell's score property definitely has a value (I used to break points to check). If I select any of the very first few tableView cells that are visible they will have a nil value for the score property. However if I scroll further down through the cells and back up then those same cells will no longer have a nil score property.
What I found out was the if let statement was running before the Firebase code was pulled so the score property was nil for first few cells that are on scene. The odd thing is everything works fine once I start scrolling.
How can I pass a value pulled directly from a cell to the tableView's didSelectRow?
PlayerModel:
class PlayerModel{
name:String?
score:String?
randomPower:String?
}
TableViewCell SubClass:
class PlayerCell:UITableViewCell{
#IBOutlet weak var nameLabel: UILabel!
#IBOutlet weak var scoreLabel: UILabel!
#IBOutlet weak var randomPowerLabel: UILabel!
internal var score: String?{
didSet{
print("**********\(score ?? "*********")")
}
}
override func prepareForReuse() {
super.prepareForReuse()
nameLabel.text = " "
scoreLabel.text = " "
}
func getScoreDataFromFirebase(){
let scoreRef = usersRef?.child("score")
scoreRef?.observe( .value, with: {
(snapshot) in
for child in snapshot.children{
let user = child as! DataSnapshot
for player in user.children{
let eachPlayer = player as! DataSnapshot
if let dict = eachPlayer.value as? [String:Any]{
let name = dict["name"] as? String ?? " "
let score = dict["score"] as? String ?? " "
self.nameLabel.text = name
self.scoreLabel.text = score
self.score = score
}
}
}
}
}
}
TableView:
class PlayerController: UITableViewDataSource, UITableViewDelegate{
#IBOutlet weak fileprivate var tableView: UITableView!
var players = [PlayerModel]() // this array is populated with data from a previous vc. The number of players in the array are the same exact number of players that's pulled from the getScoreDataFromFirebase() function
override func viewDidLoad() {
super.viewDidLoad()
tableView.reloadData()
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return players.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "PlayerCell", for: indexPath) as! PlayerCell
let cellData = players[indexPath.row]
cellData.randomPowerLabel.text = cellData.power
cell.getScoreDataFromFirebase()
if let score = cell.score{
cellData.score = score
}
return cell
}
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let indexPath = tableView.indexPathForSelectedRow else { return }
let scoreVC = storyboard?.instantiateViewController(withIdentifier: "ScoreController") as! ScoreController
scoreVC.score = players[indexPath.row].score
}
You can achieve this using delegation :
Create a protocol
protocol UpdateValueDelegate: class {
func changeValue(score: String, row: Int)
}
Your UIViewController should look like this :
PlayController : UITableViewDataSource, UITableViewDelegate, UpdateValueDelegate
{
var scoreDict:[String:String] = [:]
//
//
func changeValue(score: String, row: Int)
{
self.scoreDict["\(row)"] = score
}
}
In cellForRowAtIndexPath set cell.delegate = self and cell.row = indexPath.row
Your UITableViewCell should look like this :
class PlayerCell:UITableViewCell: UITableViewCell
{
weak var delegate: UpdateValueDelegate?
var row: Int?
//
//
}
Finally pass score from getScoreDataFromFirebase by calling delegate function:
func getScoreDataFromFirebase()
{
//
//
delegate. changeValue(score: localScore, row: self.row)
}
Now you have the value in your viewController from where it can be easily passed to didSelectRow using the global dictionary ** scoreDict**.
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)
{
var score = self.scoreDict["\(indexPath.row)"]
// Use score
}
I've got a simple UITable implementation and an array which is called fruits. Taken out of this example. The only difference is that I am using a UIViewController for the UITable insted of a UITableViewController, but this should be irrelevant for now.
class MainViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
var fruits : [String] = Array()
let thread : GetDataForFruitsArrayThread
func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return fruits.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "LabelCell", for: indexPath)
cell.textLabel?.text = fruits[indexPath.row]
return cell
}
}
Now I have got a viewDidLoad function in this class also. In the function I am calling a thread which is getting the array as a parameter. The thread is filling up the array with new values asynchronously. Means I don't know if there will be any values or when do the value appear and are set into the array. So you can imagine what I want to ask on this point: How do I signal the UITableView that I have changed the content of the array once the view has loaded the array into the UITable. In c# there is the NotifyAll() function which notfies all the Listener to an attribute. Is there an easy and performant way to do this in swift, optionally without importing any non-included libraries?
override func viewDidLoad() {
super.viewDidLoad()
self.thread = GetDataForFruitsArrayThread(array: &fruits)
self.thread.StartAsynchronously()
}
EDIT - adding GetDataForFruitsArrayThread
class GetDataForFruitsArrayThread : MyThread {
var array : [String]
init(array: inout [String] ) {
self.array = array
}
override func main() {
// init socket
while(true){
_ = withUnsafeMutablePointer(to: &rcvaddr){
$0.withMemoryRebound(to: sockaddr.self, capacity: 1){
recvfrom(m_socket, &pData, bufferlength, 0, UnsafeMutablePointer($0),
&socketAddressLength)
}
}
var fruit = String(cString:inet_ntoa(rcvaddr.sin_addr), encoding: .ascii)
self.array.append(fruit)
}
}
}
EDIT - 21.11.2017:
Following Sandeep Bhandari's answer I changed the following code in the ViewController:
#IBOutlet weak var tableView1: UITableView!
var fruits : [String] = Array() {
didSet {
self.tableView1.reloadData()
}
}
PROBLEM: I am setting a breakpoint at array.append(fruit) in the GetDataForFruitsArrayThread-class. After that there is a breakpoint in the didSet method. But this breakpoint does not get called after the append()-call! NOTE: The parameter on the init function of the GetDataForFruitsArrayThread-class is an in-out parameter!
UPDATE: I found out that if I do the array.append(fruit)-call on the GetDataForFruitsArrayThread.array in the GetDataForFruitsArrayThread-thread, the Fruits-array in ViewController-class does not get changed, therefore didSet did not get called. How can I change this behaviour?
Because you said you are running a endless process to keep updating your array and you want the UI update once the array changes you can use the below solution.
var fruits : [String] = Array() {
didSet {
self.tableView.reloadData()
}
}
What am I doing ? Simple added a setter to array and whenever value changes I reload the tableView.
EDIT:
The above solution will work only if you modify the inout fruits array you pass to GetDataForFruitsArrayThread
self.thread = GetDataForFruitsArrayThread(array: &fruits)
So don't create one more array property in GetDataForFruitsArrayThread rather directly modify the array passed to GetDataForFruitsArrayThread.
I have made a custom cell class which fetches data from firebase. Everything's working fine but what happens is when new data is added, is gets displayed at the bottom not the top. In the code i have mentioned to insert new item to index path 0. Still the code is not working
Here's the code..
class ViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate {
#IBOutlet weak var save: UIButton!
#IBOutlet weak var table: UITableView!
var firebase = Firebase(url: "https://meanwhile.firebaseio.com")
var messages = [String]()
var uid:String = ""
override func viewDidLoad() {
super.viewDidLoad()
var localArray = [String]()
firebase.observeEventType(.ChildAdded, withBlock: { snapshot in
//print(snapshot.value)
let msgText = snapshot.value.objectForKey("text") as! String
localArray.insert(msgText, atIndex: 0)
self.messages = localArray
self.table.reloadData()
}
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return messages.count
}
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = self.table.dequeueReusableCellWithIdentifier("Vish", forIndexPath: indexPath) as! CustomTableViewCell
let fdata = self.messages[indexPath.item]
cell.data.text = fdata as? String
return cell
}
}
I don't see the code here, but you mention you are using a custom table cell to load data.
If you are loading data inside of your CustomTableViewCell class, you either have to be very careful about how you do it, or move the data loading for the cells elsewhere.
This is because of dequeueReusableCellWithIdentifier. UITableView does not create a UITableViewCell for ever row of the table, but instead it maintains a smaller queue of cells that it reuses to reduce memory usage and increase performance.
The returned UITableViewCell object is frequently one that the application reuses for performance reasons.
Check if you are sharing state (that you didn't expect to be) and/or have race-conditions inside of your CustomTableViewCell.
These bugs often manifest as table view data appearing in the wrong cell.
Source: https://developer.apple.com/library/ios/documentation/UIKit/Reference/UITableViewDataSource_Protocol/index.html#//apple_ref/occ/intfm/UITableViewDataSource/tableView:cellForRowAtIndexPath: