0

I would like to retrieve data from my simple Firestore database

I have this database: enter image description here

then I have a model class where I have a method responsible for retrieving a data which looks like this:

func getDataFromDatabase() -> [String] {
    var notes: [String] = []
    collectionRef = Firestore.firestore().collection("Notes")

    collectionRef.addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
                print("Error fetching documents: \(error!)")
                return
            }
        notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
            print("inside notes: \(notes)")
    }
    print("outside notes: \(notes)")
    return notes
}

and as a UI representation I have tableViewController. Let's take one of the methods, for example

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    print("tableview numberOfRowsInSection called")
    return model.getDataFromDatabase().count
}

Then numberOfRows is 0 and the output in the console is: enter image description here

and I am ending up with no cells in tableView. I added a breakpoint and it doesn't jump inside the listener.

And even though I have 3 of them, they are kinda "late"? They are loaded afterwards. And then the tableView doesn't show anything but console says (later) that there are 3 cells.

If needed, there is also my method for showing the cells names:

 override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    print("Cells")
    let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)

    cell.textLabel!.text = String(model.getDataFromDatabase()[indexPath.row].prefix(30))

    return cell
}

but this method is not even loaded (no print in the console) and this method is written below the method with numberOfRowsInSection.

I have also 2 errors (I don't know why each line is written twice) and these are: enter image description here

but I don't think it has something to do with the problem.

Thank you for your help!

Michal Moravik
  • 1,213
  • 1
  • 12
  • 23
  • Firestore is async, so notes is returning as an empty array before the data has been downloaded from the sever. Either populate a viewController variable and add a didSet observer or add completionHandler to your `getDataFromDatabase` method – Galo Torres Sevilla Apr 03 '19 at 21:50
  • @GaloTorresSevilla could you, please, provide code solution? At least some smaller one as this is the first time I am implementing this. I understood your comment but I am not able to implement. Thank you. – Michal Moravik Apr 03 '19 at 22:07
  • @MapeSVK I just posted a solution. Hope it helps! – Bhaumik Apr 03 '19 at 22:11
  • @MapeSVK didn’t really have a computer in hand. That’s why I didn’t post an answer. But you already have a good one with @Bhaumik’s answer – Galo Torres Sevilla Apr 03 '19 at 22:25
  • http://www.programmingios.net/returning-a-value-from-asynchronous-code/ – matt Apr 07 '19 at 19:07

2 Answers2

2

As @Galo Torres Sevilla mentioned, addSnapshotListener method is async and you need to add completion handler to your getDataFromDatabase() function.

Make following changes in your code:

  1. Declare Global variable for notes.

    var list_notes = [String]()
    
  2. Add completion handler to getDataFromDatabase() method.

    func getDataFromDatabase(callback: @escaping([String]) -> Void) {
        var notes: [String] = []
        collectionRef = Firestore.firestore().collection("Notes")
    
        collectionRef.addSnapshotListener { querySnapshot, error in
            guard let documents = querySnapshot?.documents else {
                print("Error fetching documents: \(error!)")
                return
            }
            notes = documents.map { $0["text"]! } as! [String] // text is a field saved in document
            print("inside notes: \(notes)")
    
            callback(notes)
        }
    }
    
  3. Lastly, call function on appropriate location where you want to fetch notes and assign retrieved notes to your global variable and reload TableView like below:

    self.getDataFromDatabase { (list_notes) in
        self.list_notes = list_notes
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }
    

Changes in TableView:

override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    print("tableview numberOfRowsInSection called")
    return self.list_notes.count
}

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    print("Cells")
    let cell = tableView.dequeueReusableCell(withIdentifier: "firstCell", for: indexPath)

    cell.textLabel!.text = String(self.list_notes[indexPath.row].prefix(30))

    return cell
}
Bhaumik
  • 1,218
  • 1
  • 11
  • 20
  • 1
    thank you mate, it works! Just one question. Why is this so complicated? Isn't this fundamental stuff? Probably I am doing it wrong way, right? – Michal Moravik Apr 03 '19 at 22:23
  • Glad that it helped! Only issue with your code is, you were not waiting for Firestore to download your data. So, `cellForRowAt:indexPath:` method was executing before download was complete. By adding completion handler, it solves that issue. – Bhaumik Apr 03 '19 at 22:30
0

All you need to do is refresh the table cell every time you retrieve the data. Put this code after you set your data inside the array.

self.tableView.reloadData()
Dan Hadzami
  • 31
  • 1
  • 4