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()
}
Related
I apologize if this question is simple or the problem is obvious as I am still a beginner in programming.
I am looping over an array and trying to make an async Firestore call. I am using a DispatchGroup in order to wait for all iterations to complete before calling the completion.
However, the Firestore function is not even getting called. I tested with print statements and the result is the loop iterations over the array have gone through with an enter into the DispatchGroup each time and the wait is stuck.
func getUserGlobalPlays(username: String, fixtureIDs: [Int], completion: #escaping (Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { ids in
group.enter()
print("entered")
DispatchQueue.global().async { [weak self] in
self?.db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: ids).getDocuments { snapshot, error in
guard let snapshot = snapshot, error == nil else {
completion(.failure(error!))
return
}
for document in snapshot.documents {
let fixtureDoc = document.data()
let fixtureIDx = fixtureDoc["fixtureID"] as! Int
let choice = fixtureDoc["userChoice"] as! Int
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
group.leave()
print("leaving")
}
}
}
group.wait()
print(plays.count)
completion(.success(plays))
}
There are a few things going on with your code I think you should fix. You were dangerously force-unwrapping document data which you should never do. You were spinning up a bunch of Dispatch queues to make the database calls in the background, which is unnecessary and potentially problematic. The database call itself is insignificant and doesn't need to be done in the background. The snapshot return, however, can be done in the background (which this code doesn't do, so you can add that if you wish). And I don't know how you want to handle errors here. If one document gets back an error, your code sends back an error. Is that how you want to handle it?
func getUserGlobalPlays(username: String,
fixtureIDs: [Int],
completion: #escaping (_result: Result<[UserPlays]?, Error>) -> Void) {
let chunkedArray = fixtureIDs.chunked(into: 10)
var plays: [UserPlays] = []
let group = DispatchGroup()
chunkedArray.forEach { id in
group.enter()
db.collection("Users").document("\(username)").collection("userPlays").whereField("fixtureID", in: id).getDocuments { snapshot, error in
if let snapshot = snapshot {
for doc in snapshot.documents {
if let fixtureIDx = doc.get("fixtureIDx") as? Int,
let choice = doc.get("choice") as? Int {
plays.append(UserPlays(fixtureID: fixtureIDx, userChoice: choice))
}
}
} else if let error = error {
print(error)
// There was an error getting this one document. Do you want to terminate
// the entire function and pass back an error (through the completion
// handler)? Or do you want to keep going and parse whatever data you can
// parse?
}
group.leave()
}
}
// This is the completion handler of the Dispatch Group.
group.notify(queue: .main) {
completion(.success(plays))
}
}
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.
I am using Firebase as my backend and I am trying to increase a the number that is currently being held in the database by 5. However, when it is called, the database adds 5 over and over again, so the score goes from 5 to 10 to 15... this is repeated until the app crashes.
Why is this happening?
func changeUserRewardsScore() {
let db = Firestore.firestore()
db.collection("\(Auth.auth().currentUser?.uid ?? "default")").document("score")
.addSnapshotListener { (querySnapshot, error) in
if let e = error {
print("There was an issue retrieving data from Firestore. \(e)")
}
else {
if let data = querySnapshot?.data() {
let myArray = Array(data.values)
let userScore = "\(myArray[0])"
print("userScore = \(userScore)")
self.writeUserScore(score: userScore)
}
}
}
}
func writeUserScore(score: String) {
var myScore = 0
if localData.rewards.freeCookieInCart == true && score == "50" {
myScore = 0
}
else {
myScore = Int(score)!+5
}
let db = Firestore.firestore()
db.collection("\(Auth.auth().currentUser?.uid ?? "default")").document("score").setData(["score":myScore]) {
(error) in
if let e = error {
print("There was an issue saving data to firestore, \(e)")
} else {
print("Successfully saved data.")
DispatchQueue.main.async {
}
}
}
}
Your document listener, when triggered, writes back to the same document that triggered it, so it triggers again with the result of that change. Which starts the whole cycle over again.
It's not clear to me what you expect to happen instead, but if you just want to get the value of the document once, then update it, you should use get() instead of onSnapshot() as illustrated in the documentation. Either that, or set up some state in your object that indicates to your listener when it shouldn't update the document again.
I'm trying to use DispatchQueue to get my code to wait until a query retrieves the results I need from Cloud Firestore before it continues executing, but just haven't been able to get it to work. In the code below I am trying to get it to wait until the data has been retrieved and stored in the zoneMarkerArray, and then print out the result.
I've numbered each line it prints in the order that I want it to happen, and as you'll see in the output it is not waiting for the Firestore result before moving on.
Here is my code:
let zones = self.db.collection("zones")
let zonesQuery = zones.whereField("start", isGreaterThan: lowerLimit).whereField("start", isLessThan: upperLimit)
print("1. zones Query has been defined")
//pass zonesQuery query to getZoneMarkers function to retrieve the zone markers from Firestore
getZoneMarkers(zonesQuery)
print("6. Now returned from getZoneMarkers")
func getZoneMarkers(_ zonesQuery: Query) -> ([Double]) {
print("2. Entered getZoneMarkers function")
DispatchQueue.global(qos: .userInteractive).async {
zonesQuery.getDocuments() { (snapshot, error) in
if let error = error {
print("Error getting zone markers: \(error)")
} else {
print("3. Successfully Retrieved the zone markers")
var result: Double = 0.0
for document in snapshot!.documents {
print("Retrieved zone marker is \(document["start"]!)")
self.zoneMarkerArray.append(document["start"]! as! Double)
print("4. Looping over zone marker results")
}
}
}
DispatchQueue.main.async {
//I want this the printCompleted function to print the result AFTER the results have been retrieved
self.printCompleted()
}
}
return self.zoneMarkerArray
}
func printCompleted() {
print("5. Looping now completed. Result was \(zoneMarkerArray)")
}
And here is the output that prints out:
zones Query has been defined
Entered getZoneMarkers function
Now returned from getZoneMarkers
Looping now completed. Result was [0.0]
Successfully Retrieved the zone markers
Looping over zone marker results
Looping over zone marker results
Retrieved zone marker is 12.0
Looping over zone marker results
Thanks for the help!
EDIT: In case anyone else out there is also struggling with this, here's the working code I put together in the end based on the feedback I received. Please feel free to critique if you see how it could be further improved:
let zones = self.db.collection("zones")
let zonesQuery = zones.whereField("start", isGreaterThan: lowerLimit).whereField("start", isLessThan: upperLimit)
print("1. zones Query has been defined")
//pass zonesQuery query to getZoneMarkers function to retrieve the zone markers from Firestore
getZoneMarkers(zonesQuery)
func getZoneMarkers(_ zonesQuery: (Query)) {
print("2. Entered getZoneMarkers function")
zoneMarkerArray.removeAll()
zonesQuery.getDocuments(completion: { (snapshot, error) in
if let error = error {
print("Error getting zone markers: \(error)")
return
}
guard let docs = snapshot?.documents else { return }
print("3. Successfully Retrieved the zone markers")
for document in docs {
self.zoneMarkerArray.append(document["start"]! as! Double)
print("4. Looping over zone marker results")
}
self.completion(zoneMarkerArray: self.zoneMarkerArray)
})
}
func completion(zoneMarkerArray: [Double]) {
print("5. Looping now completed. Result was \(zoneMarkerArray)")
}
From the question, it doesn't appear like any DispatchQueue's are needed. Firestore asynchronous so data is only valid inside the closures following the firebase function. Also, UI calls are handled on the main thread whereas networking is on the background thread.
If you want to wait for all of the data to be loaded from Firestore, that would be done within the closure following the Firestore call. For example, suppose we have a zones collection with documents that store start and stop indicators
zones
zone_0
start: 1
stop: 3
zone_1
start: 7
stop: 9
For this example, we'll be storing the start and stop for each zone in a class array of tuples
var tupleArray = [(Int, Int)]()
and the code to read in the zones, populate the tupleArray and then do the 'next step' - printing them in this case.
func readZoneMarkers() {
let zonesQuery = self.db.collection("zones")
zonesQuery.getDocuments(completion: { documentSnapshot, error in
if let err = error {
print(err.localizedDescription)
return
}
guard let docs = documentSnapshot?.documents else { return }
for doc in docs {
let start = doc.get("start") as? Int ?? 0
let end = doc.get("end") as? Int ?? 0
let t = (start, end)
self.tupleArray.append(t)
}
//reload your tableView or collectionView here,
// or proceed to whatever the next step is
self.tupleArray.forEach { print( $0.0, $0.1) }
})
}
and the output
1 3
7 9
Because of the asynchronous nature of Firebase, you can't 'return' from a closure, but you can leverage a completion handler if needed to pass the data 'back' from the closure.
Maybe this can help you. I have a lot of users, it appends to my Model, and can check when I have all the data und go on with my code:
func allUser (completion: #escaping ([UserModel]) -> Void) {
let dispatchGroup = DispatchGroup()
var model = [UserModel]()
let db = Firestore.firestore()
let docRef = db.collection("users")
dispatchGroup.enter()
docRef.getDocuments { (querySnapshot, err) in
for document in querySnapshot!.documents {
print("disp enter")
let dic = document.data()
model.append(UserModel(dictionary: dic))
}
dispatchGroup.leave()
}
dispatchGroup.notify(queue: .main) {
completion(model)
print("completion")
}
}
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()
}