Search code examples
iosswiftgoogle-cloud-firestoreuikit

How do refresh my UITableView after reading data from FirebaseFirestore with a SnapShotListener?


UPDATE at the bottom.

I have followed the UIKit section of this Apple iOS Dev Tutorial, up to and including the Saving New Reminders section. The tutorials provide full code for download at the beginning of each section.

But, I want to get FirebaseFirestore involved. I have some other Firestore projects that work, but I always thought that I was doing something not quite right, so I'm always looking for better examples to learn from.

This is how I found Peter Friese's 3-part YT series, "Build a To-Do list with Swift UI and Firebase". While I'm not using SwiftUI, I figured that the Firestore code should probably work with just a few changes, as he creates a Repository whose sole function is to interface between app and Firestore. No UI involved. So, following his example, I added a ReminderRepository.

It doesn't work, but I'm so close. The UITableView looks empty but I know that the records are being loaded.

Stepping through in the debugger, I see that the first time the numberOfRowsInSection is called, the data hasn't been loaded from the Firestore, so it returns 0. But, eventually the code does load the data. I can see each Reminder as it's being mapped and at the end, all documents are loaded into the reminderRepository.reminders property.

But I can't figure out how to get the loadData() to make the table reload later.

ReminderRepository.swift

    class ReminderRepository {
      let remindersCollection = Firestore.firestore()
          .collection("reminders").order(by: "date")

      var reminders = [Reminder]()

      init() {
        loadData()
      }

      func loadData() {
        print ("loadData")
        remindersCollection.addSnapshotListener { (querySnapshot, error) in
            if let querySnapshot = querySnapshot {
                self.reminders = querySnapshot.documents.compactMap { document in
                    do {
                        let reminder = try document.data(as: Reminder.self)
                        print ("loadData: ", reminder?.title ?? "Unknown")
                        return reminder
                    } catch {
                        print (error)
                    }
                    return nil
                }
            }
            print ("loadData: ", self.reminders.count)
        }
    }
}

The only difference from the Apple code is that in the ListDataSource.swift file, I added:

   var remindersRepository: ReminderRepository

   override init() {
     remindersRepository = ReminderRepository()
   }

and all reminders references in that file have been changed to

remindersRepository.reminders.

Do I need to provide a callback for the init()? How? I'm still a little iffy on the matter.

UPDATE: Not a full credit solution, but getting closer.

I added two lines to ReminderListViewController.viewDidLoad() as well as the referenced function:

    refreshControl = UIRefreshControl()
    refreshControl?.addTarget(self, action: #selector(refreshTournaments(_:)), for: .valueChanged)

@objc
private func refreshTournaments(_ sender: Any) {
    tableView.reloadData()
    refreshControl?.endRefreshing()
}

Now, when staring at the initial blank table, I pull down from the top and it refreshes. Now, how can I make it do that automatically?


Solution

  • Firstly create some ReminderRepositoryDelegate protocol, that will handle communication between you Controller part (in your case ReminderListDataSource ) and your model part (in your case ReminderRepository ). Then load data by delegating controller after reminder is set. here are some steps:

    1. creating delegate protocol.

       protocol ReminderRepositoryDelegate: AnyObject {
       func reloadYourData()
      

      }

    2. Conform ReminderListDataSource to delegate protocol:

       class ReminderListDataSource: UITableViewDataSource, ReminderRepositoryDelegate {
       func reloadYourData() {
           self.tableView.reloadData()
       }
      

      }

    3. Add delegate weak variable to ReminderRepository that will weakly hold your controller.

         class ReminderRepository {
               let remindersCollection = Firestore.firestore()
                   .collection("reminders").order(by: "date")
      
               var reminders = [Reminder]()
               weak var delegate: ReminderRepositoryDelegate?
               init() {
                   loadData()
               }
      

      }

    4. set ReminderListDataSource as a delegate when creating ReminderRepository

       override init() {
       remindersRepository = ReminderRepository()
       remindersRepository.delegate = self
      

      }

    5. load data after reminder is set

       func loadData() {
       print ("loadData")
       remindersCollection.addSnapshotListener { (querySnapshot, error) in
           if let querySnapshot = querySnapshot {
               self.reminders = querySnapshot.documents.compactMap { document in
                   do {
                       let reminder = try document.data(as: Reminder.self)
                       print ("loadData: ", reminder?.title ?? "Unknown")
                       delegate?.reloadYourData()
                       return reminder
                   } catch {
                       print (error)
                   }
                   return nil
               }
           }
           print ("loadData: ", self.reminders.count)
       }
      

      }