SwiftUI Firestore query cursors pagination not working - ios

I'm trying to implement pagination in my app. when the app loads I want to load first item lets say. then every time he clicks refresh a new item is loaded.
I have implemented this logic in my viewmodel.
First time homeViewModel.load() is called in my SceneDelegate
let homeViewModel = HomeViewModel()
homeViewModel.refresh()
Then every time users wants to get new dates I call homeViewModel.refresh()
The problem is that when the app loads I do not get any results and when I hit refresh I keep getting the second document in the table over and over again.
What am I doing wrong here?
My HomeViewModel:
class HomeViewModel: ObservableObject, LoadProtocol {
var firestoreService: FirestoreService = FirestoreService()
#Published var items: [Item] = []
let db = Firestore.firestore()
var first: Query = Firestore.firestore().collection("items").limit(to: 1)
load() {
self.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
}
// Construct a new query starting after this document,
// retrieving the next 25 cities.
let next = self.db.collection("items")
.start(afterDocument: lastSnapshot).limit(to: 1)
self.first = next
// Use the query for pagination.
}
}
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)
}
}
}
}

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

How to get all subcollections from Firestore documents?

I have collection users with documents and subcollections records in each of them. How I can get list of all subcollections of records and then merge it in one global collection? How this can be done?
I've listener to get documents from one subcollection:
func recordsObserve(records: [MRecords], completion: #escaping (Result<[MRecords], Error>) -> Void) -> ListenerRegistration? {
var records = records
let recordsRef = db.collection(["users", currentUserId, "records"].joined(separator: "/"))
let recordsListener = recordsRef.addSnapshotListener { (querySnapshot, error) in
guard let snapshot = querySnapshot else {
print(1)
completion(.failure(error!))
return
}
snapshot.documentChanges.forEach { (diff) in
guard let record = MRecords(document: diff.document) else { return }
switch diff.type {
case .added:
guard !records.contains(record) else { return }
records.append(record)
case .modified:
guard let index = records.firstIndex(of: record) else { return }
records[index] = record
case .removed:
guard let index = records.firstIndex(of: record) else { return }
records.remove(at: index)
}
}
completion(.success(records))
}
return recordsListener
}
But I need listener to get all subcollections of records from all users.
What you're looking for is known as a collection group query in Firestore. To get all documents from all records collection, it'd look something like this:
db.collectionGroup("records").getDocuments { (snapshot, error) in
// ...
}
While looping over the documents in the snapshot, you can get the document ID of the user document by using the parent property of the record's DocumentReference and CollectionReference.

Nested Async functions in for loops

I'm getting confused with nested async calls in Swift using Firebase Firestore. I'm trying to make a friends page for my app, and part of the page is a UITableView of groups of the users' friends. I'm storing these groups within a separate collection in Firebase, and attempting to get a list of the groups the current user is in from the document IDs in the group collection. Right now, that looks like this:
func createGroupsArray(completion: #escaping ([Group]?) -> Void) {
let dispatchGroup1 = DispatchGroup()
let dispatchGroup2 = DispatchGroup()
var groups = [Group]()
let currentUser = User.current
guard currentUser.groupDocStrings.count > 0 else {
return completion(nil)
}
for g in currentUser.groupDocStrings {
dispatchGroup1.enter()
FirestoreService.db.collection(Constants.Firestore.Collections.groups).document(g).getDocument { (snapshot, err) in
if let err = err {
print("Error retrieving group document: \(err)")
return completion(nil)
} else {
let data = snapshot?.data()
var friends = [User]()
for f in data![Constants.Firestore.Keys.users] as! [String] {
dispatchGroup2.enter()
FirestoreService.db.collection(Constants.Firestore.Collections.users).document(f).getDocument { (snapshot, err) in
if let err = err {
print("Error retrieving user document: \(err)")
return completion(nil)
} else {
let uData = snapshot?.data()
friends.append(User(uid: f, data: uData!))
}
dispatchGroup2.leave()
}
}
dispatchGroup2.notify(queue: .main) {
let group = Group(groupName: data![Constants.Firestore.Keys.groupName] as! String, friends: friends)
groups.append(group)
}
dispatchGroup1.leave()
}
}
}
dispatchGroup1.notify(queue: .main) {
completion(groups)
}
}
But of course, when I go to call this function in my tableView(cellForRowAt) function, I can't return a cell because it's asynchronous. I feel like there must be a better way to do this, any help?
Keep track for every row whether you have data for it or not. Make cellForRowAt() return a cell, with data if you have data, without data if you don't. When you downloaded the data for a row, store the data, remember that you have the data, and invalidate the row. cellForRowAt() will be called again, and this time you fill it with the right data.
Do NOT remember the cell object, because by the time your async call returns, it may not contain the data of the same row anymore. And if you can add or remove rows or change the sort order then do NOT remember the row number, because by the time your async call returns, it may not be the same row number anymore.

Can't pass variable value from firebase firestore to another class SWIFT

So I have this function in class Functions :
struct Prices {
var standardPrice: Int!
}
// FUNC PRICING
class Functions {
private var PricingRef: CollectionReference!
var price = Prices()
func getPrice() -> Prices {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
}
}
}
return price
}
}
Then I want to pass the standardPrice value to this class, called PriceList :
class PriceList: UITableViewController {
var price = Prices()
var newStandardPrice = 0
func Price() {
price = Functions().getPrice()
newStandardPrice = price.standardPrice // always error with value nil
}
I always have that error where newStandardPrice is nil.
but the print(self.price.standardPrice!) shows number of result I want.
So as far as I know, the problem here is because it takes time for the firebase firestore to get the data from database.
How do I get the value of standardPrice after its assigned with the new price from firebase database?
Any help will be appreciated
Thankyou
you need to use completion handler because its async function
func getPrice(completion:#escaping (Prices?,Error?)-> Void) {
PricingRef = Firestore.firestore().collection("ProductXYZ")
PricingRef.getDocuments { (snapshot, error) in
if let err = error {
debugPrint("Error fetching data \(err)")
completion(nil,err)
}
else {
guard let snap = snapshot else { return }
for document in snap.documents {
let data = document.data()
let std = data["standard"] as! String
self.price.standardPrice = Int(std)!
print(self.price.standardPrice!) // This print the intended result
completion(self.price.standardPrice,nil)
}
}
}
}
How to use
Functions().getPrice { (price, error) in
if let err = error {
// do something if you get error
} else if let getPrice = price {
// use price
self.price = getPriice
}

Documents fetched are replacing the previous loaded documents in tableview

I am using the below code to fetch the data from the firestore database in swift iOS. But when I scroll the new data loaded is replacing the previously loaded data in the tableview. I am trying to fix this issue but as of now no good.
The outcome required is that adding new documents to the previously list of documents in the tableview
Below is the code I am implementing. If any more information is required please let me know
CODE
fileprivate func observeQuery() {
fetchMoreIngredients = true
//guard let query = query else { return }
var query1 = query
stopObserving()
if posts.isEmpty{
query1 = Firestore.firestore().collection("posts").order(by: "timestamp", descending: true).limit(to: 5)
}
else {
query1 = Firestore.firestore().collection("posts").order(by: "timestamp", descending: true).start(afterDocument: lastDocumentSnapshot).limit(to: 2)
// query = db.collection("rides").order(by: "price").start(afterDocument: lastDocumentSnapshot).limit(to: 4)
print("Next 4 rides loaded")
print("hello")
}
// Display data from Firestore, part one
listener = query1!.addSnapshotListener { [unowned self] (snapshot, error) in
guard let snapshot = snapshot else {
print("Error fetching snapshot results: \(error!)")
return
}
let models = snapshot.documents.map { (document) -> Post in
if let model = Post(dictionary: document.data()) {
return model
} else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Post.self) with dictionary \(document.data())")
}
}
self.posts = models
self.documents = snapshot.documents
if self.documents.count > 0 {
self.tableView.backgroundView = nil
} else {
self.tableView.backgroundView = self.backgroundView
}
self.tableView.reloadData()
self.fetchMoreIngredients = false
self.lastDocumentSnapshot = snapshot.documents.last
}
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
let off = scrollView.contentOffset.y
let off1 = scrollView.contentSize.height
if off > off1 - scrollView.frame.height * leadingScreensForBatching{
if !fetchMoreIngredients && !reachEnd{
print(fetchMoreIngredients)
// beginBatchFetch()
// query = baseQuery()
observeQuery()
}
}
}
Instead of calling snapshot.documents, call snapshot.documentChanges. This returns a list of document changes (either .added, .modified, or .removed, and allows you to add, remove, or modify them in your local array as needed... Not tested code just an idea what you ca do ...
snapshot.documentChanges.forEach() { diff in
switch diff.type {
case .added:
if let model = Post(dictionary: diff.document.data()){
self.posts.append(model)
}else {
// Don't use fatalError here in a real app.
fatalError("Unable to initialize type \(Post.self) with dictionary \(document.data())")
}
case .removed:
// add remove case
case .modified:
// add modify case
}
}

How can I convert the downloaded data from Firestore to an array and assign it to my table view?

I'm capturing Firestore data as Firebase shows us, but I don't know how to save the query I make.
In conclusion, what I want to do is bring all the documents that have the same value in your pid field, and then show in a table the product fields and start date, each document in a different cell.
collection food
document: 1
pid:john1
product:Ice
startDate:01/01/2010
document: 2
pid:john1
product:Rice
startDate:01/02/2010
I need to show in the table:
Ice was bought on 01/01/2010
Rice was bought on 01/02/2010
I have this code:
func loadFood(){
pid = "john1"
db = Firestore.firestore()
db.collection("food").whereField("pid", isEqualTo: pid)
.addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("\n--------------------------------------")
print("Error document: \(error!)")
print("--------------------------------------\n")
return
}
let startDate = documents.map { $0["startDate"]! }
let product = documents.map {$0["product"]!}
let message = ("\(product) was bought \(startDate)")
self.dataRecord.insert(message, at: 0)
DispatchQueue.main.async {
self.tvRecord.reloadData()
}
}
}
I'm showing in the table:
[Ice, Rice] was bought on [01/01/2010, 01/02/2010]
You make a couple of mistakes. First, you loop over the documents multiple times, unnecessarily; that's not very efficient. In your case, you should loop over them once and do all of your data prep in each loop iteration. Second, Firestore has a method specifically for extracting data from document fields called get() which is very easy to read and efficient.
func loadFood(){
pid = "john1"
Firestore.firestore().collection("food").whereField("pid", isEqualTo: pid).addSnapshotListener { querySnapshot, error in
guard let documents = querySnapshot?.documents else {
print("\n--------------------------------------")
print("Error document: \(error!)")
print("--------------------------------------\n")
return
}
for doc in documents {
guard let startDate = doc.get("startDate") as? String,
let product = doc.get("product") as? String else {
continue // continue this loop, not "return" which will return control out of the calling function
}
let message = "\(product) was bought \(startDate)"
dataRecord.append(message)
}
DispatchQueue.main.async {
self.tvRecord.reloadData()
}
}
}
As you deal with the 2 arrays as if they are 1 string instead you need
struct Item {
let message,startDate:String
}
Then
var dataRecord = [Item]()
and finally
self.dataRecord = documents.map { Item(message:$0["product"]!,startDate:$0["startDate"]!)}

Resources