Pagination with Firebase Firestore - Swift 4

Pagination with Firebase firestore - swift 4

So here's the solution I've come up with! It is very likely that this solution makes multiple calls to firestore, creating a large bill for any real project, but it works as a proof of concept I guess you could say.

If you have any recommendations or edits, please feel free to share!

Here's how all the variables were initialized:

var rides = [Ride]()
var lastDocumentSnapshot: DocumentSnapshot!
var fetchingMore = false

If you have any recommendations or edits, please feel free to share!

func scrollViewDidScroll(_ scrollView: UIScrollView) {
let offsetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height
//print("offsetY: \(offsetY) | contHeight-scrollViewHeight: \(contentHeight-scrollView.frame.height)")
if offsetY > contentHeight - scrollView.frame.height - 50 {
// Bottom of the screen is reached
if !fetchingMore {
paginateData()
}
}
}

// Paginates data
func paginateData() {

fetchingMore = true

var query: Query!

if rides.isEmpty {
query = db.collection("rides").order(by: "price").limit(to: 6)
print("First 6 rides loaded")
} else {
query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
print("Next 4 rides loaded")
}

query.getDocuments { (snapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
} else if snapshot!.isEmpty {
self.fetchingMore = false
return
} else {
let newRides = snapshot!.documents.compactMap({Ride(dictionary: $0.data())})
self.rides.append(contentsOf: newRides)

//
DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: {
self.tableView.reloadData()
self.fetchingMore = false
})

self.lastDocumentSnapshot = snapshot!.documents.last
}
}
}

Swift Firestore pagination is retrieving the next batch of documents but replacing the first batch

I think the problem comes from the call to self.glimpse.removeAll() here:

GLIMPSE_ALL_USERS_DATA.order(by: TIMESTAMP, descending: true).limit(to: 3)
.getDocuments { (snapshot, error) in

guard let last = snapshot?.documents.last else { return }
guard let allObjects = snapshot?.documents else { return }

allObjects.forEach( { document in
self.glimpse.removeAll()
self.glimpse = Glimpse.parseData(snapshot: snapshot)
...

Since you first remove all data from self.glimpse, the previous page of data is removed after you add the new one.

To keep both pages, don't call self.glimpse.removeAll().


As you pointed out in the comments, your second page is loaded by the else block - so the above can't cause that problem.

In the else block you also replace the existing value of self.glimpse with each snapshot though in this line:

self.glimpse = Glimpse.parseData(snapshot: snapshot)

So: whatever value self.glimpse had before that line, it'll be gone after the line has run.

I'd expect to see something where you add the Glimpse.parseData(snapshot: snapshot) to self.glimpse instead of replacing it each time.

Pagination Firestor CollectionView Swift Programmatically


 func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
if newInstrumentList.count-1 == indexPath.row && isMoreDataAvailable{
self.presenter?.showMorePage()
}
}

This is what is use for pagination, generally during an API call I get the total number of item and depending on that I set a bool property. Whenever the collection view cell index matches with the indexPath.row, next data is requested.

Firestore Pagination get next batch of data

The problem is that you are mixing Basic Cursor Pagination with Query Cursor Pagination on your implementation.

The Basic Cursor Pagination Part of your code (up to the first.addSnapshotListener) is correct and will execute everytime returning more and more data to your map every time you call the function and resolve it, however the Query Cursor Pagination part will never be called, so you can remove that part completely from your implementation.

So your code should look like:

func fetchCollection<Collection: CollectionProtocol>(lastSnapshot: DocumentSnapshot?, query: Query, completion: @escaping (Result<[Collection], Error>) -> Void) {
var first = query.limit(to: 1)

if let lastSnapshot = lastSnapshot {
first = query.start(afterDocument: lastSnapshot)
}

first.getDocuments() { (querySnapshot, error) in
if let snapshot = querySnapshot {
// print(snapshot.metadata.isFromCache)
completion(.success(snapshot.documents.map { document -> Collection in
return Collection(document: document)
}))
} else if let error = error {
completion(.failure(error))
}
}
}

NOTE: You might also add the .limit(to:1) to your lastSnapshot check, since the way it is now, it will retrieve all the documents, of course, if that is your intended logic, ignore this note.


EDIT:

If you want to use Query Cursor Pagination, you have can follow this example where the listener is located outside of the fetchCollection function and it's executed everytime a getDocuments() is triggered and mounts the new query to be executed:

//initial state of your app
var results = "nil";

//initial query
var query = db.collection("foo").document("bar").limit(to: 1);

//everytime you need more data fetched and on database updates to your snapshot this will be triggered
query.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach {
// update data
}
let next = db.collection("foo").document("bar").start(afterDocument: result.last!).limit(to: 1);
query = next;
};

//initial load of data
fetchCollection();

//simply executes the query
func fetchCollection() {
query.getDocuments { (document, error) in
if let document = document, document.exists {
if(result == "nil"){
result = document.data().map();
}else{
result.map({ document.data() })
}
} else {
print("Document does not exist")
}

}
}

NOTE: Keep in mind that this is a untested example but it might be a good starting point to what you need in your app, Also, I use this documentation for the realtime updates on the listener, you might find some more information on that link.

Firestore Querying with pagination- Swift

I figured it out, it's because I was using the .getDocuments call instead of the .addsnapshotListener call. However, If anyone knows How I can accomplish the same thing whilst keeping the .getDocuments call, i'm open ears!

Not able to paginate firestore filter based query

The problem in this case is that you are trying to order by an attribute and perform a where clause with an "equals" condition on a different attribute.

If you want to perform this kind of operation you have to create a composite index for both attributes.

SwiftUI Firestore query cursors pagination not working

Here's your refresh function

func refresh() {
self.firestoreService.fetchCollection(query: self.first) { (result: Result<[Item], Error>) in
switch result {
case .success(let items):
self.items += items
self.addToCategories()
case .failure(let error):
print(error)
}
}
}

There's nothing in that function that advances the cursor further so it will read the same data over and over.

If you want to read the next set of data, the curser need to be moved to the last document after each refresh, like this

func refresh() {
self.first.addSnapshotListener { (snapshot, error) in
if let err = error {
print(err.localizedDescription)
return
}

guard let snapshot = snapshot else { return }

guard let lastSnapshot = snapshot.documents.last else { return }

let next = self.db.collection("items").start(afterDocument: lastSnapshot).limit(to: 2)

self.first = next

for doc in snapshot.documents {
print(doc.documentID)
}
}
}


Related Topics



Leave a reply



Submit