Novice programmer, currently working on an ios app using firebase as our backend. I\'m trying to grab values from the firebase database to populate a TableView, but there are tw
Firebase is asynchronous: it takes time for values to be returned from Firebase via a query or observe call.
Your code isn't allowing for that so whats happening is the observeSingleEvent is called and the cellForRowAt function is returning (an empty) cell before Firebase has a chance to return the data and populate committee.
Code is way faster than the internet so you need to operate on data within the observe blocks (closures) as that's the time when it's valid.
Best practice is to populate the datasource within an Firebase observe block (closure) - here are the steps
and to sort by Head Char, here's a swifty solution. Assume the dict is populated with .values from Firebase
let dict = ["0": ["name": "First Disarmament and International Security Committee",
"Head Chair": "Jessie Mao"],
"1": ["name": "UN Special, Political And Decolonization Committee",
"Head Chair": "Trevor Dowds"],
"2": ["name": "UN 6th Legal Committee",
"Head Chair": "Benjy Malings"]
]
let sortedArray = Array(dict).sorted { $0.1["Head Chair"]! < $1.1["Head Chair"]! }
Can you try to change this:
let sectionRef = FIRDatabase.database().reference().child(String(indexPath.section))
to this:
let sectionRef = FIRDatabase.database().reference(withPath: "Committee").child(String(indexPath.section))
In general: It's best practise to create a function for your download purposes and call the function instead of putting all the lines of code inside your cell declaration:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "committeeCell", for: indexPath) as! CommitteeTableViewCell
// we are passing the Label and the indexPath to out func
myFirebaseFunc(label: cell.committeeLabel, indexPath: indexPath)
return cell
}
Also you should use observeEventType
, to handle the case, that there might be data added to your Firebase during runtime and you want the data to be added to your UITableView
.
func myFirebaseFunc(label: UILabel, indexPath: NSIndexPath) {
let sectionRef = FIRDatabase.database().reference(withPath: "Committee".child(String(indexPath.section))
let committeeRef = sectionRef.child(String(indexPath.row))
let currentRef = committeeRef.child(String(indexPath.row)).child("name")
currentRef.observeEventType(of: .value, withBlock: { (snapshot) in
if snapshot.exists() {
label?.text = snapshot.value as? String
}
})
}
To your case: Also I assume that your App crashes because indexPath out of bounds
. It's not best practise to download your Firebase data according to your indexPath within the population of the Cell. Since, as stated correctly by Jay, the download is async.
You have to download your desired data first and then populate a tableView
according to your data like this:
You create an Array of Models (struct) or NSMutableDictionary.
// first you check, if your snapshot exists
if snapshot.exists() {
// you delete your Array, you your data will not be added multiple
// times once you add data in the background and the func gets
// called again
self.myArray.removeAll()
// you sort your snapshot according to your needs (for example of the (pre set) date value. If date is missing, the code will not crash!
let sorted = (snapshot.value!.allValues as NSArray).sortedArrayUsingDescriptors([NSSortDescriptor(key: "date",ascending: false)])
// then you loop through your sorted Array
for element in sorted {
// you create a name String of your name element in your Dictionary
let name = element.valueForKey("name")! as? String
// if you have created a struct, you instantiate a model and set the name
let m = MyStruct(name: name!)
// and you append it to your Array
self.myArray.append(m)
}
// last but not least we reload our tableView on the main threat
DispatchQueue.main.async{
self.tableView.reloadData()
}
}
Then you set numbersOfRowsInSection
to myArray.count
And fill your tableView with:
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "committeeCell", for: indexPath) as! CommitteeTableViewCell
let mine = myArray[indexPath.row]
cell.committeeLabel.text = mine.name
return cell
}
This would be our struct in this example:
struct MyStuct {
var name: String = ""
}
Our Array instantiated in the ViewController class:
let myArray: [MyStruct] = []
In this best practise scenario we would call our myFirebaseFunc
func at the viewDidAppear
func of our ViewController.