How can we paginate a firebase / firestore query on ios swift? - ios

I try to find a solution for paginate a firebase query on ios/swift but I couldn't build algorithm for my state.
My method is like this:
func downloadData(completion: #escaping ([Post]) -> Void) {
// download data with pagination
let firestoreDatabase = Firestore.firestore()
var first = firestoreDatabase.collection("posts").order(by: "date", descending: true).limit(to: 5)
first.addSnapshotListener{ snapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// getting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
// how can I repeat this query as long as lastSnapshot exist
firestoreDatabase.collection("posts").order(by: "date", descending: true).start(afterDocument: lastSnapshot).addSnapshotListener { querySnapshot, error in
}
}
}
I tried following mindset but it didn't work, and entered an infinite loop. I didn't understand why it is.
func downloadData(completion: #escaping ([Post]) -> Void) {
// download data with pagination
let firestoreDatabase = Firestore.firestore()
var first = firestoreDatabase.collection("posts").order(by: "date", descending: true).limit(to: 5)
first.addSnapshotListener{ snapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// geting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
repeat {
firestoreDatabase.collection("posts").order(by: "date", descending: true).start(afterDocument: lastSnapshot).addSnapshotListener { querySnapshot, error in
guard let snapshot = snapshot else {
print("Error retrieving cities: \(error.debugDescription)")
return
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
return
}
self.postList.removeAll(keepingCapacity: false)
DispatchQueue.global().async {
for document in snapshot.documents {
// getting data from document stuff ...
self.postList.append(self.post)
}
completion(self.postList)
}
lastSnapshot = snapshot.documents.last
}
} while(lastSnapshot.exists)
}
}
I think lastSnapshot must be nil after the query loop but it is appear that it is still exist.
how can I fix lastSnapshot problem? Or is there different mindset / easiest way to paginate?
In firebase documents, it says just use this but how can we repeat query that has " .start(afterDocument: lastSnapshot) " stuff?

First and foremost, for plain-vanilla pagination, don't use a snapshot listener when fetching documents. You can paginate documents with a snapshot listener but the process is more complex.
I've embedded my notes into the comments in the code below for clarity.
let pageSize = 5
var cursor: DocumentSnapshot?
func getFirstPage(completion: #escaping (_ posts: [Post]?) -> Void) {
let db = Firestore.firestore()
let firstPage = db.collection("posts").order(by: "date", descending: true).limit(to: pageSize)
firstPage.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
// Don't leave the caller hanging on errors; return nil,
// return a Result, throw an error, do something.
completion(nil)
if let error = error {
print(error)
}
return
}
guard !snapshot.isEmpty else {
// There are no results and so there can be no more
// results to paginate; nil the cursor.
cursor = nil
// And don't leave the caller hanging, even on no
// results; return an empty array.
completion([])
return
}
// Before parsing the snapshot, manage the cursor.
if snapshot.count < pageSize {
// This snapshot is smaller than a page size and so
// there can be no more results to paginate; nil
// the cursor.
cursor = nil
} else {
// This snapshot is a full page size and so there
// could potentially be more results to paginate;
// set the cursor.
cursor = snapshot.documents.last
}
var posts: [Post] = []
for doc in snapshot.documents {
posts.append(newPost) // pseudo code
}
completion(posts)
}
}
func continuePages(completion: #escaping (_ posts: [Post]?) -> Void) {
guard let cursor = cursor else {
return
}
let db = Firestore.firestore()
let nextPage = db.collection("posts").order(by: "date", descending: true).limit(to: pageSize).start(afterDocument: cursor)
nextPage.getDocuments { snapshot, error in
guard let snapshot = snapshot else {
completion(nil)
if let error = error {
print(error)
}
return
}
guard !snapshot.isEmpty else {
// There are no results and so there can be no more
// results to paginate; nil the cursor.
cursor = nil
completion([])
return
}
// Before parsing the snapshot, manage the cursor.
if snapshot.count < pageSize {
// This snapshot is smaller than a page size and so
// there can be no more results to paginate; nil
// the cursor.
cursor = nil
} else {
// This snapshot is a full page size and so there
// could potentially be more results to paginate;
// set the cursor.
cursor = snapshot.documents.last
}
var morePosts: [Post] = []
for doc in snapshot.documents {
morePosts.append(newPost) // pseudo code
}
completion(morePosts)
}
}

Related

Firestore iOS - Ordering collection by field in document

I have an array called "homeList" which observers "CURRENT_USER_FRIENDS_REF" collection and places it in the array. How can I make it so I can order this array by the "timestamp" field found in the document snapshot.
homeList array function
var homeList = [User]()
func addHomeObserver(_ update: #escaping () -> Void) {
CURRENT_USER_FRIENDS_REF.getDocuments { snapshot, error in
self.homeList.removeAll()
guard error == nil else {
#if DEBUG
print("Error retrieving collection")
#endif
return
}
let group = DispatchGroup()
for document in snapshot!.documents {
let whosfrom = document.get("fromId") as? String
let id = document.documentID
**let timestamp = document.get("timestamp") as? NSNumber**
group.enter()
self.getUser(id, completion: { (user) in
if whosfrom != self.CURRENT_USER_ID {
self.homeList.append(user)
}
group.leave()
})
}
group.notify(queue: .main) {
update()
}
}
}
Current user friends reference:
var CURRENT_USER_FRIENDS_REF: CollectionReference {
return CURRENT_USER_REF.collection("friends")
}
Thanks.
You can use order(by on a collection reference to get the result.
CURRENT_USER_FRIENDS_REF.order(by: "timestamp", descending: true).getDocuments { snapshot, error in
}

Optional Still Returning Nil After Assigning Value

I am working on a similar feature to 'liking/unliking a post'.
I have an MVVM architecture as;
struct MyStructModel {
var isLiked: Bool? = false
}
class MyStructView {
var isLiked: Bool
init(myStructModel: MyStructModel) {
self.isLiked = myStructModel.isLiked ?? false
}
}
I successfully get the value of whether the post is liked or not here;
func isPostLiked(documentID: String, completion: #escaping (Bool) -> Void) {
guard let authID = auth.id else { return }
let query = reference(to: .users).document(authID).collection("liked").document(documentID)
query.getDocument { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let data = snapshot?.data() else { return }
if let value = data["isLiked"] as? Bool {
completion(value)
} else {
completion(false)
}
}
}
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { (snapshot, error) in
if error != nil {
print(error as Any)
return
}
guard let snapshotDocuments = snapshot?.documents else { return }
for document in snapshotDocuments {
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
// isLiked is nil here...
self.isPostLiked(documentID: post.documentID!) { (isLiked) in
post.isLiked = isLiked
print("MODEL SAYS: \(post.isLiked!)")
// isLiked is correct value here...
}
posts.append(post)
}
completion(posts)
}
}
}
However, when it gets to my cell the value is still nil.
Adding Cell Code:
var post: MyStructView? {
didSet {
guard let post = post else { return }
print(post.isLiked!)
}
}
Your isLiked property is likely nil in your cells because the retrieveReviews function doesn't wait for the isPostLiked function to complete before completing itself.
You could easily solve this issue by using DispatchGroups. This would allow you to make sure all of your Posts have their isLiked value properly set before being inserted in the array, and then simply use the DispatchGroup's notify block to return all the loaded posts via the completion handler:
func retrieveReviews(completion: #escaping([MyStructModel]) -> ()) {
var posts = [MyStructModel]()
let query = reference(to: .posts).order(by: "createdAt", descending: true)
query.getDocuments { [weak self] (snapshot, error) in
guard let self = self else { return }
if error != nil {
return
}
guard let documents = snapshot?.documents else { return }
let dispatchGroup = DispatchGroup()
for document in documents {
dispatchGroup.enter()
if var post = try? JSONDecoder().decodeQuery(MyStructModel.self, fromJSONObject: document.decode()) {
self.isPostLiked(documentID: post.documentID!) { isLiked in
post.isLiked = isLiked
posts.append(post)
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
completion(posts)
}
}
}

Firestore Pagination query not returning all results

I have implemented pagination firestore query to return results 5 at a time.
It first starts by creating a listener and pulling 5 results. On the pull to refresh it pulls 5 more results. On the 2nd pull to refresh it for some reason pulls 10 results. On the 3rd pull to refresh it doesn't pull any results. After looking at the data some data is missing.
Below is my MessageListener function:
func createMessageListener() {
reference = db.collection(["chats", channel.id!, "messages"].joined(separator: "/"))
let first = reference?.order(by: "created", descending: true).limit(to: 5)
messageListener = first!.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
return
}
snapshot.documentChanges.forEach { change in
self.handleDocumentChange(change)
self.updateReadStatus()
}
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
self.hasMoreMessages = false
return
}
self.last = self.reference?.order(by: "created", descending: true).start(afterDocument: lastSnapshot).limit(to: 5)
}
}
Below is my loadMoreMessages function:
func loadMoreMessages() {
if hasMoreMessages {
last!.addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error listening for channel updates: \(error?.localizedDescription ?? "No error")")
self.hasMoreMessages = false
self.delegate.viewModelDidLoadMoreMessages(hasMoreMessages: self.hasMoreMessages)
return
}
self.moreMessagesLoaded = true
snapshot.documentChanges.forEach { change in
self.handleDocumentChange(change)
}
self.delegate.viewModelDidLoadMoreMessages(hasMoreMessages: true)
guard let lastSnapshot = snapshot.documents.last else {
// The collection is empty.
self.hasMoreMessages = false
return
}
self.last = self.reference?.start(afterDocument: lastSnapshot)
}
} else {
self.delegate.viewModelDidLoadMoreMessages(hasMoreMessages: self.hasMoreMessages)
}
}
Not too sure what is going on. If in the create listener function i change the first limit to 35 for example. It will pull all the data.
Thanks in advance for any help

Proper way to use Firestore diff

I'm Calling this function to retrieve users from firestore:
Each time a user is modify I want to update the users array.
func fetchUsers( complete: #escaping ( _ success: Bool, _ users: [User], _ error: Error? )->()) {
//self.users = []
let circleId = UserDefaults.standard.string(forKey: "circleId") ?? ""
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documentChanges.forEach { diff in
if (diff.type == .added) {
let data = diff.document.data()
let id = diff.document.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
if (diff.type == .modified) {
// Update users array if user data is modified
if !self.users.isEmpty {
self.users = []
let data = diff.document.data()
let id = diff.document.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
}
if (diff.type == .removed) {
print("Removed user: \(diff.document.data())")
}
}
}
}
However my array always return 1 if there's only one user modified and my collectionview reload and then only show one user! How do I
return all the users even if only one was modified?
Thanks
New Function:
func fetchUsers( complete: #escaping ( _ success: Bool, _ users: [User], _ error: Error? )->()) {
self.users = []
let circleId = UserDefaults.standard.string(forKey: "circleId") ?? ""
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documents.forEach { diff in
let data = diff.data()
let id = diff.documentID
let user = User(key: id, data: data)
self.users.append(user)
complete(true, self.users, nil)
}
}
}
Now I'm seeing a lot of duplicate users in my collectionview
You're looping over QuerySnapshot.documentChanges, which only contains documents that changed since the last snapshot.
To get all documents in the query (instead of just the modified ones), loop over QuerySnapshot.documents instead:
DataService.call.REF_CIRCLES.document(circleId).collection("insiders").order(by: "position", descending: false).addSnapshotListener { querySnapshot, error in
guard let snapshot = querySnapshot else {
print("Error fetching snapshots: \(error!)")
return
}
snapshot.documents.forEach { diff in
...

Swift Firebase - get current user with document UID to populate list

So I have done this on my android app (and it works), to populate a list with the document names from a collection
db.collection("usersAuth/${FirebaseAuth.getInstance().uid!!}/KitLists")
.addSnapshotListener(EventListener<QuerySnapshot> { value, e ->
if (e != null) {
Log.w("TAG", "Listen failed.", e)
return#EventListener
}
for (document in value.documents) {
val data = document
val kitName = data.id
firstKitList.add(kitName)
}
mainListViewAdapter.notifyDataSetChanged()
})
I am trying to do the same on my iOS version but I don't know whats wrong
override func viewWillAppear(_ animated: Bool) {
setListener()
}
func setListener() {
db.collection("usersAuth/\(String(describing: Auth.auth().currentUser))/KitLists")
.addSnapshotListener { (snapshot, error ) in
if let err = error {
debugPrint("Error fetching docs: \(err)")
} else {
guard let snap = snapshot else {return}
for document in snap.documents {
let data = document.data()
let kitListName = data["KitLists"] as? String
let newLists = KitList(kitListName: kitListName!)
self.lists.append(newLists)
}
self.tableView.reloadData()
}
}
}
any ideas? Thanks
-- EDIT
Firestore
Firestore2
You need to get the uid from the currentUser, for example:
if let userId = Auth.auth().currentUser.uid {
db.collection("usersAuth").document(userId).collection("KitLists")
.addSnapshotListener { (snapshot, error ) in
//...
}
To get the KitLists documentId
for document in snap.documents {
let documentName = document.documentID // <--- This
let newLists = KitList(kitListName: documentName)
self.lists.append(newLists)
}

Resources