Search code examples
swiftgoogle-cloud-firestoreuikitcompletionhandlerdispatchgroup

DispatchGroup to wait for Firestore query completion


I am running multiple Firestore queries in a single user tapGesture, which requires me to ensure that there are minimum to no simultaneous Firestore queries running in the app. I have read multiple answers(Waiting until the task finishes) on this issue, but my queries are not running in the desired Sequence as I intended to.

I would appreciate your help to guide me on using DispatchGroup to ensure logical sequence of codes & queries.

I want to ensure that the the Firestore query within function checkAndCreateUserWorkoutProfile is completed before the 2 print statements within DispatchGroup.notify(...), starting with "Firestore Sequence" method are initiated. Instead, the query completed after the two print statements were initiated.

Below is my code and screenshot of Xcode debugger. As shown in the screenshot, the problem is that using "DispatchGroup.notify(...)" is not waiting until the Firestore query within the function checkAndCreateUserWorkoutProfile is completed.

alert.addAction(UIAlertAction(title: "Select", style: .default, handler: { [self]_ in
            
            let dispatchGroup = DispatchGroup()
            
            dispatchGroup.enter()
            
            DispatchQueue.main.async {
                print("Firestore Sequence 2 Initiated")
                startActivityIndicator()
                
                if let user = Auth.auth().currentUser {
                    let userID = user.uid
                    
                    db.collection("user").whereField("author_uid", isEqualTo: userID).getDocuments { snapshot, error in
                        
                        if error == nil && snapshot != nil {
                            
                            for document in snapshot!.documents {
                            
                            let docID = document.documentID
                            
                            db.collection("user")
                                .document(docID)
                                .setData(["selectedWorkoutID" : workoutRow.workoutId], merge: true)
                                    
                            print("Firestore Sequence 2 Success - user selectedWorkoutID updated")
                                
                            }
                        }
                        dispatchGroup.leave()
                        
                        if let user = Auth.auth().currentUser {
                            let userID = user.uid
                            checkAndCreateUserWorkoutProfile(selectedWorkout: workoutRow, userID: userID)
                        }
                    }
                }
            }

            dispatchGroup.notify(queue: .main) {
                print("Firestore Sequence 4 Initiated")
                print("Firestore Sequence 5 Initiated - Create/Read User Specific Dayprogram data")
                }

        }))

func checkAndCreateUserWorkoutProfile(selectedWorkout: Workout, userID: String) {

        print("Firestore Sequence 3 Initiated - createORread user specific workout profile")
        
        let dispatchGroup = DispatchGroup()
        
        dispatchGroup.enter()
        
        db.collection("Workout")
            .whereField("workoutId", isEqualTo: selectedWorkout.workoutId)
            .whereField("userID", isEqualTo: userID)
            .getDocuments() { (querySnapshot, err) in
                
                if querySnapshot?.count == 0 {

                    var ref: DocumentReference? = nil
                  
                    ref = self.db.collection("Workout").addDocument(data:
                        [
                        "author_uid": selectedWorkout.author_uid!,
                        "workoutId": selectedWorkout.workoutId,
                        "userID": userID
                        ])

                     { err in
                        if let err = err {
                            print("Error adding user specific workout profile: \(err)")
                            dispatchGroup.leave()
                        } else {
                            print("Firestore Sequence 3 Success - User specific workout profile added/created with ID: \(ref!.documentID)")
                            dispatchGroup.leave()
                        }
                    }
                    
                    
                } 
                
            }

        }

enter image description here

With @Kiril S.'s answer, codes were corrected as shown below.

class WorkoutViewController: UIViewController {

let dispatchGroup = DispatchGroup()


alert.addAction(UIAlertAction(title: "Select", style: .default, handler: { [self]_ in
            
            dispatchGroup.enter()
            
            DispatchQueue.main.async {
                print("Firestore Sequence 2 Initiated")
                startActivityIndicator()
                
                if let user = Auth.auth().currentUser {
                    let userID = user.uid
                    
                    db.collection("user").whereField("author_uid", isEqualTo: userID).getDocuments { snapshot, error in
                        
                        if error == nil && snapshot != nil {
                            
                            for document in snapshot!.documents {
                            
                            let docID = document.documentID
                            
                            db.collection("user")
                                .document(docID)
                                .setData(["selectedWorkoutID" : workoutRow.workoutId], merge: true)
                                    
                            print("Firestore Sequence 2 Success - user selectedWorkoutID updated")
                                
                            }
                        }
                        
                        
                        if let user = Auth.auth().currentUser {
                            let userID = user.uid
                            checkAndCreateUserWorkoutProfile(selectedWorkout: workoutRow, userID: userID)
                         dispatchGroup.leave()
                        }
                    }
                }
            }

            dispatchGroup.notify(queue: .main) {
                print("Firestore Sequence 4 Initiated")
                print("Firestore Sequence 5 Initiated - Create/Read User Specific Dayprogram data")
                }

        }))

func checkAndCreateUserWorkoutProfile(selectedWorkout: Workout, userID: String) {

        print("Firestore Sequence 3 Initiated - createORread user specific workout profile")
        
        dispatchGroup.enter()
        
        db.collection("Workout")
            .whereField("workoutId", isEqualTo: selectedWorkout.workoutId)
            .whereField("userID", isEqualTo: userID)
            .getDocuments() { (querySnapshot, err) in
                
                if querySnapshot?.count == 0 {

                    var ref: DocumentReference? = nil
                  
                    ref = self.db.collection("Workout").addDocument(data:
                        [
                        "author_uid": selectedWorkout.author_uid!,
                        "workoutId": selectedWorkout.workoutId,
                        "userID": userID
                        ])

                     { err in
                        if let err = err {
                            print("Error adding user specific workout profile: \(err)")
                            self.dispatchGroup.leave()
                        } else {
                            print("Firestore Sequence 3 Success - User specific workout profile added/created with ID: \(ref!.documentID)")
                            self.dispatchGroup.leave()
                        }
                    }     
                }      
            }
        }

Xcode Console showing corrected sequence of codes/queries. enter image description here


Solution

  • So basically what happens now:

    • Alert action starts
    • Creates let dispatchGroup = DispatchGroup()
    • Enters that group
    • Leaves the group ==> dispatchGroup.notify fires
    • checkAndCreateUserWorkoutProfile starts, creates its own dispatch group and so on (already wrong, doesn't matter)

    So you have 2 problems:

    1. You have to have 1 dispatch group, not a separate group for each function. So move let dispatchGroup = DispatchGroup() to class member level, that way both functions are in the same dispatch group.

    2. And your checkAndCreateUserWorkoutProfile needs to enter dispatch group before previous call leaves it. So the order should be changed to

      if let user = Auth.auth().currentUser {
          let userID = user.uid
          checkAndCreateUserWorkoutProfile(selectedWorkout: workoutRow, userID: userID)
      }
      dispatchGroup.leave()
      

    That way:

    • Alert action starts
    • Enters a group defined as a class member
    • Calls checkAndCreateUserWorkoutProfile, which enters the same dispatch group and starts its async call
    • Alert action leaves the group (but notification doesn't fire, since checkAndCreateUserWorkoutProfile didn't leave the group yet)
    • checkAndCreateUserWorkoutProfile leaves the group eventually ==> dispatchGroup.notify fires