How to Stop Firestore Pagination When Searched Data Are Loaded in Tableview

How to stop Firestore pagination when searched data are loaded in tableView

SOLUTION

I finally found the solution. In the searchIngredients(text: String) function I should have declared the fetchMoreIngredients as true in the beginning and within the closure.

func searchIngredients(text: String){

fetchMoreIngredients = true

let db = Firestore.firestore()

db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
if let err = err {
print("\(err.localizedDescription)")
print("Test Error")
} else {
if (querySnapshot!.isEmpty == false){
self.searchedIngredientsArray = querySnapshot!.documents.compactMap({Ingredients(dictionary: $0.data())})
self.ingredientsArray = self.searchedIngredientsArray
self.tableView.reloadData()
self.fetchMoreIngredients = true

}else{
print("No Ingredients were found")
}
}
}

}

Pagination infinitely calling in TableView - Swift/Xcode

The challenge here is that the actual dataset in Firebase Firestore has an unknown amount of data, so without knowing what the 'last row' is, there's no easy way to know you're displaying the 'last row' when paginating

A couple of options:

A common solution is to have another collection that stores a count of documents for the collection you're displaying, and observe that collection for changes so you always know what the last row is and can not attempt to load more data when you're displaying the last row of data.

For example suppose your app displays users

users_collection
user_0
name: "Jay"
user_1
name: "Cindy"
user_2
name: "Larry"

and then a collection that keeps track of the number of users

document_count
users_collection
count: 3

Add an observer to the document_count, users_collection and as users are added or removed, increment/decrement that count, keep a class var to make that count available to functions, call it var totalRows = 0

Then, it becomes a matter of only calling paginate when the user scrolls to the last row in the array AND there's more data available

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt...
// Trigger pagination when scrolled to last cell & there's more rows
if (indexPath.row == fileArray.count - 1 && lastRowDisplayedIndex < self.totalRows) {
paginate()
}
}

Note this assumes you're tracking what rows from Firestore are being displayed in some form.

Another option is to keep a count of how many documents were read in on the last query

query.getDocuments() { (querySnapshot, err) in
self.lastReadCount = snapshot.count

then when the user attempts to scroll, paginate will only be called if the lastReadCount is equal to the number you want displayed.

For Example

Suppose you want to display 5 users at a time. If 5 users are being displayed then call paginate to attempt to read 5 more, if only 3 are read then self.lastReadCount will be 3 and you'll know you're at the end of the list, so don't call paginate.

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 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
}
}
}

SearchBar problem while trying to search Firestore and reload the tableview

The issue in your question is that Firestore is asynchronous.

It takes time for Firestore to return documents you've requested and that data will only be valid within the closure calling the function. The code outside the closure will execute way before the data is available within the closure.

So here's what's going on.

func searchIngredients(text: String) -> Array<Any>{
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
}
//the code below here will execute *before* the code in the above closure
self.tableView.reloadData()
ingredientsArray = searchedIngredientsArray
return ingredientsArray
}

what's happening is the tableView is being refreshed before there's any data in the array.

You're also returning the ingredientsArray before it's populated. More importantly, attempting to return a value from an asynchronous function can (and should) generally be avoided.

The fix is to handle the data within the closure

class ViewController: NSViewController {
var ingredientArray = [String]()
func searchIngredients(text: String) {
let db = Firestore.firestore()
db.collection("Ingredients").whereField("compName", arrayContains: text).getDocuments{ (querySnapshot, err) in
//the data has returned from firebase and is valid
//populate the class var array with data from firebase
// self.ingredientArray.append(some string)
//refresh the tableview
}
}

Note that the searchIngredients function should not return a value - nor does it need to



Related Topics



Leave a reply



Submit