Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Firebase Firestore Pagination with Swift

With my app i tried to paginate my data (10 posts per page) from Firestore using below code,

import UIKit
import FirebaseFirestore
class Home: UITableViewController {


    var postArray = [postObject]()
    let db = Firestore.firestore()
    var page : DocumentSnapshot? = nil
    let pagingSpinner = UIActivityIndicatorView(activityIndicatorStyle: .gray)

    override func viewDidLoad() {
        super.viewDidLoad()

       loadFirstPage()
    }


    func loadFirstPage(){

        // Get the first 10 posts

        db.collection("POSTS").limit(to: 10).addSnapshotListener { (snapshot, error) in
            if snapshot != nil {
                self.postArray = (snapshot?.documents.flatMap({postObject(dec : $0.data())}))!

                // Save the last Document

                self.page = snapshot?.documents.last
                self.tableView.reloadData()
            }
        }
    }

    func loadNextPage(){

       // get the next 10 posts

        db.collection("POSTS").limit(to: 10).start(afterDocument: page!).addSnapshotListener { (snapshot, error) in
            if snapshot != nil {

                for doc in (snapshot?.documents)! {

                    self.postArray.append(postObject(dec: doc.data()))
                }

                self.page = snapshot?.documents.last

                self.tableView.reloadData()

            }

        }

    }


    override func numberOfSections(in tableView: UITableView) -> Int {

        return 1
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {

        return postArray.count
    }


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

        // display data

        cell?.textLabel?.text = postArray[indexPath.row].name

        return cell!
    }


    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {

        // check index to load next page

        if indexPath.row < (self.postArray.count){


            pagingSpinner.startAnimating()
            pagingSpinner.color = UIColor.red
            pagingSpinner.hidesWhenStopped = true
            tableView.tableFooterView = pagingSpinner
            loadNextPage()


        }

    }


}

But i have faced the following issues :

  • if i start to post something for the first time (FireStore has no data at all) from other devices the app will crash because the page will always be nil.
  • I tried to insert 10 post by the console and check the app when I start scrolling down with my table view it will crash for the same reason page is nil.

I'm wondering why is this happening although I'm saving the last Sanpshot document as pagination cursor ! is there a better why to implement the pagination with Swift

like image 288
Mohammed Riyadh Avatar asked Nov 10 '17 10:11

Mohammed Riyadh


1 Answers

Paginating in Firestore with Swift is very straightforward if we get documents manually (using getDocuments) and not automatically (using addSnapshotListener).

I think it's wise to split the loading of data (the first page) from the continuing of data (the additional pages) for readability. firestoreQuery is a Query object that you must obviously construct on your own.

class SomeViewController: UIViewController {
    private var cursor: DocumentSnapshot?
    private let pageSize = 10 // use this for the document-limit value in the query
    private var dataMayContinue = true
    
    /* This method grabs the first page of documents. */
    private func loadData() {
        firestoreQuery.getDocuments(completion: { (snapshot, error) in
            ...
            /* At some point after you've unwrapped the snapshot,
               manage the cursor. */
            if snapshot.count < pageSize {
                /* This return had less than 10 documents, therefore
                   there are no more possible documents to fetch and
                   thus there is no cursor. */
                self.cursor = nil
            } else {
                /* This return had at least 10 documents, therefore
                   there may be more documents to fetch which makes
                   the last document in this snapshot the cursor. */
                self.cursor = snapshot.documents.last
            }
            ...
        })
    }
    
    /* This method continues to paginate documents. */
    private func continueData() {
        guard dataMayContinue,
              let cursor = cursor else {
            return
        }
        dataMayContinue = false /* Because scrolling to bottom will cause this method to be called
                                   in rapid succession, use a boolean flag to limit this method
                                   to one call. */

        firestoreQuery.start(afterDocument: cursor).getDocuments(completion: { (snapshot, error) in
            ...
            /* Always update the cursor whenever Firestore returns
             whether it's loading data or continuing data. */
            if snapshot.count < self.pageSize {
                self.cursor = nil
            } else {
                self.cursor = snapshot.documents.last
            }
            ...
            /* Whenever we exit this method, reset dataMayContinue to true. */
        })
    }
}

/* Let's assume you paginate with infinite scroll which means continuing data
   when the user scrolls to the bottom of the table or collection view. */
extension SomeViewController {
    /* Standard scroll-view delegate */
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let contentSize = scrollView.contentSize.height
        
        if contentSize - scrollView.contentOffset.y <= scrollView.bounds.height {
            didScrollToBottom()
        }
    }
    
    private func didScrollToBottom() {
        continueData()
    }
}

We can still paginate with a snapshot listener but it requires more steps. That is because when a snapshot listener returns, it will return a single page of documents, and if the user has paginated through multiple pages then the update will reset the user back to a single page. The remedy is to keep track of how many pages are rendered on screen and load that many underneath the user when a snapshot listener returns before refreshing the UI. There are additional steps beyond this, however, such as handling a new snapshot return in the middle of a UI refresh; this race condition requires stringent serialization, which may or may not require a semaphore or something as effective.

like image 73
liquid LFG UKRAINE Avatar answered Oct 04 '22 03:10

liquid LFG UKRAINE