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.
Related
I have a tableview with Restaurants. Each restaurants contains different types of food. This is my tableview for food in the restaurants, and this this is my Cart.
I want to make a "add to cart", so, when i'm tapping on the + button, it send the data from the curent cell to the database, and in the cart view controller it receives the data.
The problem :
My database is keep populating with items, and whenever ill open the app, it keeps recieving all the data, for all devices. How can i do so, when im sign in with another account, the cart to have 0 items.
If you need other infos, ask and ill edit.
Help please
Here s how im sending the data to the database when tapping the + button :
func updateDocument(rootCollection : String, newValueDict: [String : Any], completion:#escaping (Bool) -> Void = {_ in }) {
let db = Firestore.firestore()
db.collection(rootCollection).document().setData(newValueDict, merge: true){ err in
if let err = err {
print("Error writing document: \(err)")
completion(false)
}else{
completion(true)
}
}
}
This is called in cellForRowAt
{
cell.didTapButton = {
self.updateDocument(rootCollection: "CartDatabase", newValueDict: ["foodCart" : mancare.foodName, "photoKeyCart": mancare.photoKeyRestaurant, "priceCart": mancare.priceFood])
}
This is what im using for retrieving the data in the Cart Table View
and called it in viewDidLoad()
func getCartProducts() {
let db = Firestore.firestore()
db.collection("CartDatabase").getDocuments { (snapshot, error) in
if let error = error {
print(error)
return
} else {
for document in snapshot!.documents {
let data = document.data()
let newEntry = Cart(photoKeyCart: data["photoKeyCart"] as! String, foodCart: data["foodCart"] as! String , priceCart: data["priceCart"] as! Int
)
self.cart.append(newEntry)
}
}
DispatchQueue.main.async {
// self.datas = self.filteredData
self.cartTableView.reloadData()
}
}
}
I am trying to query data from firebase inside a for loop, my problem is since the queries take time to connect, swift is jumping over the queries and coming back later to do them. This creates the problem where my loop counter is ticking up but the queries are being saved for later, when the queries finally do get executed, the counter variable is all out of wack.
Where the code is being skipped is right after the query, where I am trying to append to an array.
func getSelectedData() {
var exerciseIndex = 0
for i in 0...Master.exercises.count - 1 {
if Master.exercises[i].name == self.exerciseName {
exerciseIndex = i
}
}
let numOfSets = Master.exercises[exerciseIndex].totalSets
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
dataSet.append(dataSetStruct())
dataSet[count].date = returnedExercises[count]
for number in 0...(numOfSets - 1) {
// Retrives the reps
let repsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
}
else {
// error
}
}
//Retrives the weights
let weightsDbCallHistory = db.collection("users").document("\(userId)").collection("ExerciseData").document("AllExercises").collection(exerciseName).document(returnedExercises[count]).collection("Set\(number + 1)").document("weights")
weightsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].weightsArray.append(data["Weight\(number + 1)"] as! Float)
self.updateGraph()
}
else {
// error
}
}
}
}
}
I even tried breaking out the query into another function but this doesn't seem to fix the issue.
Any help is appreciated, thanks.
EDIT:
func getSelectedData() {
if returnedExercises.count > 0 {
// Create a dispatch group
let group = DispatchGroup()
print("Getting Data")
// For each date record
for count in 0...self.returnedExercises.count-1 {
// Creates a new dataSet
self.dataSet.append(dataSetStruct())
self.dataSet[count].date = self.returnedExercises[count]
for number in 0...(self.numOfSets - 1) {
print("At record \(count), set \(number)")
// Enter the group
group.enter()
// Start the dispatch
DispatchQueue.global().async {
// Retrives the reps
let repsDbCallHistory = self.db.collection("users").document("\(self.userId)").collection("ExerciseData").document("AllExercises").collection(self.exerciseName).document(self.returnedExercises[count]).collection("Set\(number + 1)").document("reps")
repsDbCallHistory.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.dataSet[count].repsArray.append(data["Reps\(number + 1)"] as! Int)
print("Getting data: \(number)")
group.leave()
}
else {
// error
}
}
}
group.wait()
print("Finished getting data")
}
}
I tried to simplify the function for now and only have one database call in the function to try the dispatch groups. I am not sure why firebase is doing this but the code never executes the group.leave, the program just sits idle. If I am doing something wrong please let me know, thanks.
This is what the print statements are showing:
Getting Data
At record 0, set 0
At record 0, set 1
At record 0, set 2
At record 1, set 0
At record 1, set 1
At record 1, set 2
print("Getting data: (number)") is never being executed for some reason.
I am thinking that maybe firebase calls are ran on a separate thread or something, which would made them pause execution as well, but that's just my theory
EDIT2::
func getOneRepMax(completion: #escaping (_ message: String) -> Void) {
if returnedOneRepMax.count > 0 {
print("Getting Data")
// For each date record
for count in 0...self.returnedOneRepMax.count-1 {
// Creates a new dataSet
oneRPDataSet.append(oneRepMaxStruct())
oneRPDataSet[count].date = returnedOneRepMax[count]
// Retrives the reps
let oneRepMax = db.collection("users").document("\(userId)").collection("UserInputData").document("OneRepMax").collection(exerciseName).document(returnedOneRepMax[count])
oneRepMax.getDocument { (document, error) in
if let document = document, document.exists {
// For every document (Set) in the database, copy the values and add them to the array
let data:[String:Any] = document.data()!
self.oneRPDataSet[count].weight = Float(data["Weight"] as! String)!
print("Getting data: \(count)")
completion("DONE")
self.updateGraph()
}
else {
// error
}
}
}
}
}
I tried using completion handlers for a different function and it is also not working properly.
self.getOneRepMax(completion: { message in
print(message)
})
print("Finished getting data")
The order that the print statements should go:
Getting Data
Getting data: 0
Done
Getting data: 1
Done
Finished getting data
The order that the print statements are coming out right now:
Getting Data
Finished getting data
Getting data: 1
Done
Getting data: 0
Done
I am not even sure how it is possible that the count is backwards since my for loop counts up, what mistake am I making?
I think what you need are Dispatch Groups.
let dispatchGroup1 = DispatchGroup()
let dispatchGroup2 = DispatchGroup()
dispatchGroup1.enter()
firebaseRequest1() { (_, _) in
doThings()
dispatchGroup1.leave()
}
dispatchGroup2.enter()
dispatchGroup1.notify(queue: .main) {
firebaseRequest2() { (_, _ ) in
doThings()
dispatchGroup2.leave()
}
dispatchGroup2.notify(queue: .main) {
completionHandler()
}
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"]!)}
How can I get ids documents from firestore?
Now I get several ids documents from backend and me need display received ids documents in tableview.
In firestore i have this ids:
xNlguCptKllobZ9XD5m1
uKDbeWxn9llz52WbWj37
82s6W3so0RAKPZFzGyl6
EF6jhVgDr52MhOILAAwf
FXtsMKOTvlVhJjVCBFj8
JtThFuT4qoK4TWJGtr3n
TL1fOBgIlX5C7qcSShGu
UkZq3Uul5etclKepRjJF
aGzLEsEGjNA9nwc4VudD
dZp0qITGVlYUCFw0dS8C
n0zizZzw7WTLpXxcZNC6
And for example my backend found only this ids:
JtThFuT4qoK4TWJGtr3n
TL1fOBgIlX5C7qcSShGu
UkZq3Uul5etclKepRjJF
or
aGzLEsEGjNA9nwc4VudD
dZp0qITGVlYUCFw0dS8C
n0zizZzw7WTLpXxcZNC6
Me need display only this three ids in tableview. (But in reality backend return me 100+ ids and below you can see frantic sorting these ids)
Backend append this ids in temporary array var tempIds: [String] = []
So how I can get from firestore only those ids and display their in tableview?
I use this code:
fileprivate func query(ids: String) {
Firestore.firestore().collection(...).document(ids).getDocument{ (document, error) in
if let doc = document, doc.exists {
if let newModel = Halls(dictionary: doc.data()!, id: doc.documentID) {
self.halls.append(newModel)
self.halls.shuffle()
self.halls.sort(by: { $0.priority > $1.priority })
self.tableView.reloadData()
} else {
fatalError("Fatal error")
}
} else {
return
}
}
}
Me need to process ids from backend in background and after process need to show processed ids in tableview without frantic sorting.
May be need use addSnapshotListened, but I don't understand how.
UPDATED CODE:
for id in idsList {
dispatchGroup.enter()
Firestore.firestore().collection(...).document(id).getDocument{ (document, error) in
if let doc = document, doc.exists {
if let newHallModel = Halls(dictionary: doc.data()!, id: doc.documentID) {
self.tempHalls.append(newHallModel)
dispatchGroup.leave()
} else {
fatalError("Fatal error")
}
} else {
print("Document does not exist")
MBProgressHUD.hide(for: self.view, animated: true)
return
}
}
}
dispatchGroup.notify(queue: .global(qos: .default), execute: {
self.halls = self.tempHalls
DispatchQueue.main.async {
MBProgressHUD.hide(for: self.view, animated: true)
self.tableView.reloadData()
}
})
Instead of getting documents one-by-one,
you could use "IN" query to get 10 docs with 1 request:
Google Firestore - How to get several documents by multiple ids in one round-trip?
userCollection.where('uid', 'in', ["1231","222","2131"]);
// or
myCollection.where(FieldPath.documentId(), 'in', ["123","456","789"]);
// previously it was
// myCollection.where(firestore.FieldPath.documentId(), 'in', ["123","456","789"]);
Firestore Docs:
"Use the in operator to combine up to 10 equality (==) clauses on the same field with a logical OR. An in query returns documents where the given field matches any of the comparison values"
https://firebase.google.com/docs/firestore/query-data/queries
Getting a document by its identifier should be used when you need a single document or documents you cannot (based on your data architecture) query for. Don't be hesitant to denormalize your data to make queries work, that's the point of NoSQL. If I were you, I'd either add a field to these documents that can be queried or denormalize this data set with a new collection (just for this query). However, if you still choose to fetch multiple documents by identifier, then you need to make n getDocument requests and use a dispatch group to handle the asyncing:
let docIds = ["JtThFuT4qoK4TWJGtr3n", "TL1fOBgIlX5C7qcSShGu", "UkZq3Uul5etclKepRjJF"]
let d = DispatchGroup()
for id in docIds {
d.enter()
Firestore.firestore().collection(...).document(id).getDocument{ (document, error) in
// append to array
d.leave()
}
}
d.notify(queue: .global(), execute: {
// hand off to another array if this table is ever refreshed on the fly
DispatchQueue.main.async {
// reload table
}
})
All the dispatch group does is keep a count of the number of times it's entered and left and when they match, it calls its notify(queue:execute:) method (its completion handler).
I've faced the same task. And there is no better solution. Fetching documents one by one, so I've written small extension:
extension CollectionReference {
typealias MultiDocumentFetchCompletion = ([String: Result<[String: Any], Error>]) -> Void
class func fetchDocuments(with ids: [String], in collection: CollectionReference, completion:#escaping MultiDocumentFetchCompletion) -> Bool {
guard ids.count > 0, ids.count <= 50 else { return false }
var results = [String: Result<[String: Any], Error>]()
for documentId in ids {
collection.document(documentId).getDocument(completion: { (documentSnapshot, error) in
if let documentData = documentSnapshot?.data() {
results[documentId] = .success(documentData)
} else {
results[documentId] = .failure(NSError(domain: "FIRCollectionReference", code: 0, userInfo: nil))
}
if results.count == ids.count {
completion(results)
}
})
}
return true
}
}
Swift5 and Combine:
func getRegisteredUsers(usersId: [String]) -> AnyPublisher<[RegisteredUser], Error> {
return Future<[RegisteredUser], Error> { promise in
self.db.collection("registeredUsers")
.whereField(FieldPath.documentID(), in: usersId)
.getDocuments { snapshot, error in
do {
let regUsers = try snapshot?.documents.compactMap {
try $0.data(as: RegisteredUser.self)
}
promise(.success(regUsers ?? []))
} catch {
promise(.failure(.default(description: error.localizedDescription)))
}
}
}
.eraseToAnyPublisher()
}
user
29384092840923
chatRoomsJoined
chatRoom1
chatroom5
chatrooms
chatRoom1
users
29384092840923
298340982039490
I'm trying to load a tableview with information about the chat rooms a user has joined into. In the case above, user "29384092840923" has joined into chatRoom1, and I need the count of children of the users node in chatRoom1
My initial strat was to get an array of the joinedChatRooms from the "user" node and then do a for loop through and do a getDocument on each of the items in the array.
static func loadFavoriteRooms(forUID uid: String, completedFetch: #escaping (_ favoritedRoomsArray : [String]?, _ error : Error?)->()) {
let userFavoritesRef = database.collection("users").document(uid).collection("favoritedRooms")
userFavoritesRef.getDocuments { (snapshot, error) in
if error != nil {
completedFetch(nil, error!)
print("There was an error", error!.localizedDescription)
} else {
var roomArray = [String]()
for document in snapshot!.documents {
//Create a roomRef with the documentID, do a getDocument with it, and create an object with it?
let roomName = document.documentID
roomArray.append(roomName)
}
completedFetch(roomArray, nil)
}
}
}
My problem with what happened above was once I started sending off additional getDocument requests within the for-loop for the individual roomRefs, my completedFetch completion call was returning before the for loop was done asynchronously, and I wasn't getting a filled array back.
What's the cleanest way to do this? Do I need to do a dispatch group here or is there a better way to accomplish this? Using dispatch groups with firestore seems wrong here to me for some reason.
One possible option could be to use a DispatchGroup. Something like -
var roomArray = [String]()
let dispatchGroup = DispatchGroup()
for document in snapshot!.documents {
let roomId = document.documentID
let roomRef = database.collection("rooms").document(roomId)
dispatchGroup.enter()
roomRef.getDocument { (roomSnapshot, error) in
// Create the room from the snapshot here
roomArray.append(roomName)
dispatchGroup.leave()
}
}
dispatchGroup.notify(queue: .main, execute: {
completedFetch(roomArray, nil)
})
Just make sure you got your .enter() and .leave() calls correct otherwise you'll get some very strange crashes.