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 :
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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With