Learn Courses My Dashboard

Using a Completion Handler to Return Data From a Query

I’m stuck debugging a function that is behaving weirdly and I’m starting to think it has to do with threads?

The Goal:
Create a view that lets a user connect to a particular collection in my database that has already been created. That way two people can both add and view the same collection of data even though they are separate users.

What I Have Working
When a user signs up, I have a view that asks the user if they would like to enter a code.

The View Controller for this screen, takes the user’s input and validates that it’s in the right format. Then it passes it to a KitchenManager struct that I’ve made.

What I Expect the KitchenManager.getKitchen Function To Do
The KitchenManager should attempt to query the database for that particular document. If it has an error with the query, it should return the localized description of that error. If no document exists with that ID, it should return the “Kitchen not found” error. If it finds the ID, it should return nil to the VC that called it and assign the id it was given to a property in the KitchenManager.

What It’s Doing
Right now, it calls .getDocument and immediately jumps to the return errorMessage at the end of the getKitchen function. It returns to the VC, updates the UI incorrectly (since it thinks the errorMessage is nil), then comes back to the KitchenManager, runs the code inside getDocuments, and sets the errorMessage correctly. It never goes back to update the UI though.

My Current Theory
So I think what’s happening, is that the doc takes too long to get so it jumps to the end of the function and updates the UI in the VC? Then once it gets the doc, I think it returns to run the code inside the function? Is there a way to avoid this using threads? I don’t want it to return back to the VC until it knows if that document exists or not.

I am having the exact same problem and came here to post a question about it, lol.

Maybe something changed in the Firebase code?

My code:

func getWorldID() -> String {
    
    var returnValue = ""

    let loggedIn = Auth.auth().currentUser != nil ? true : false
    
    if loggedIn {  // get the user's record so that we can pull the worldID
        
        let db = Firestore.firestore()
        
        let ref = db.collection("users").document(Auth.auth().currentUser!.uid)
        
        ref.getDocument { snapshot, error in
            
            // Check there's no errors
            // THIS CODE NEVER GETS CALLED. "ref.getDocument" doesn't get here, it drop down to the return statement
            if (error != nil)
            {
                print(error!.localizedDescription)
            }
            else {
                
                // Parse the data out and set the user meta data
                let data = snapshot!.data()
                
                returnValue = data?["code"] as? String ?? ""

            }
            
        }
        
    }
    
    
    return returnValue
}

When you step through your code, does it come back to the center of the getDocument function later like mine does?

No, if I put a breakpoint on the “if (error != nil)” line, it never stops, and print commands on both sides of that conditional are never called. Something is happening during “getDocument” (which, interestingly, isn’t color coded by Xcode – all of the other calls in that function are color coded, but that line is not) that’s causing the runtime to just skip everything in that closure.

@Philomath This isn’t a threading error, first of all, it’s related to how closures work.

It’s performing exactly as I would expect it to, with how you have it written. This was a difficult concept for me to understand at the beginning too.

Don’t return errorMessage at the bottom, it’s “jumping to the bottom” because query.getDocuments is an asynchronous function, meaning it could return at anytime, and not immediately.

So it does start to run query.getDocuments, but the code does continue to run (which you’re returning the errorMessage). Remove return errorMessage

You’ll need to use a completion handler, a closure from getDocuments

Also next time, copy your code as text, with the </> button, rather than a screenshot, so it’s easier to copy your code for providing a solution

@adjensen it’s skipping because of the return at the bottom your code does start the closure, but doesn’t get to finish because it’s returning early from your return returnValue

Thanks for the reply, Mikaelacaron. Would you mind making the changes to my code to solve the problem? I’m fairly new to Swift (though I have about 20 years experience with Objective-C,) so even with knowing what the problem is, I have no idea what the solution is.

Ah! That makes sense! Thank you for explaining what I was seeing. I didn’t realize that closures worked that way.

I know the completion handler syntax is like this, but I’m having trouble figuring out where in my code I should put the error messages now. Where should I store and return the error messages? Would my completion handler take parameters or have a return value, maybe? I can’t visualize it.

    static func getKitchen(id: String, completion: @escaping() -> Void) -> String? {
        
        let db = Firestore.firestore()
        let kitchens = db.collection("kitchens")
        let query = kitchens.whereField("kitchenID", isEqualTo: id)
        var errorMessage: String? = nil
        
        query.getDocuments { (querySnapshot, error) in
            
            if let error = error {
                print(error.localizedDescription)
                errorMessage = error.localizedDescription
            }
            else {
                if querySnapshot!.documents.isEmpty {
                    print("No data Found")
                    errorMessage = "Kitchen not found. Make sure you typed the code exactly. Capitalization matters."
                } else {
                    print("Data found. ID assigned to KitchenManager.userKitchenID")
                    self.userKitchenID = id
                }
            }
        }
        completion()
    }

Should @adjensen also use a completion handler, like me?

Most likely, yes

You shouldn’t have a return type at all, you’ll instead use the completion handler to return the value. Also this will change what the function looks like at the call site

static func getKitchen(id: String, completion: @escaping (String? -> Void)) {
        
        let db = Firestore.firestore()
        let kitchens = db.collection("kitchens")
        let query = kitchens.whereField("kitchenID", isEqualTo: id)
        var errorMessage: String? = nil // not needed
        
        query.getDocuments { (querySnapshot, error) in
            
            if let error = error {
                print(error.localizedDescription)
                errorMessage = error.localizedDescription
// Use a completion handler to return the value, remove the errorMessage
completion(error.localizedDescription)
            }
            else {
                if querySnapshot!.documents.isEmpty {
                    print("No data Found")
                    errorMessage = "Kitchen not found. Make sure you typed the code exactly. Capitalization matters."
// use completion handler, same as above
completion(errorMessage)
                } else {
                    print("Data found. ID assigned to KitchenManager.userKitchenID")
                    self.userKitchenID = id
                }
            }
        }
// I can’t tell because I didn’t put your code into Xcode, but I don’t think you should return an empty completion handler. Also, if this is outside the query.getDocuments (which it looks like it is), don’t do this, the code will always “skip” to the bottom because the whole function getDocuments kicks off, and then starts to do what’s after it. And then later eventually comes and finishes its task
        completion()
    }

From what I’ve heard completion handlers (closures) are kinda the same thing as “block syntax” in Objective-C, but I’m not 100% sure how accurate that is cause I don’t have Obj-C experience

Thank you so much! I think I get it now! I didn’t know what the purpose of

completion()

was in my code! Now I see that it’s kind of like a special return statement. It says, this is the point in the code where I want you to return to the completion handler. And you can pass parameters into that “special return statement”.

For anyone who is also figuring out completion handlers, here’s what my final code ended up being!

View Controller IB Action Code - When Continue Button is Clicked

    @IBAction func continueButtonClicked(_ sender: UIButton) {
        
        if connectKitchenSegmentedControl.selectedSegmentIndex == 0 {
            
            // User is creating a new kitchen
            let kitchenID = KitchenManager.createNewKitchen()
            print(kitchenID)
            
        } else {
            // User is connecting to a kitchen
            let error = validateFields()
            
            if error != nil {
                errorLabel.text = error
                errorLabel.alpha = 1
            } else {
                // Get cleaned kitchenID from user
                let kitchenID = kitchenCodeTextField.text!.trimmingCharacters(in: .whitespacesAndNewlines)
                
                // Test if kitchen exists
                KitchenManager.getKitchen(id: kitchenID, completion: {errorMessage in
                    // Display errorMessage if the kitchenManager found one
                    if errorMessage != nil {
                        self.errorLabel.text = errorMessage
                        self.errorLabel.alpha = 1
                    }
                    else {
                        // Transition to home screen
                        self.transitionToHomeView()
                    }
                })
            }
        }
    }

KitchenManager Function With Completion Handler

    static func getKitchen(id: String, completion: @escaping(_ errorMessage: String?) -> Void){
        
        let db = Firestore.firestore()
        let kitchens = db.collection("kitchens")
        let query = kitchens.whereField("kitchenID", isEqualTo: id)
        
        query.getDocuments { (querySnapshot, error) in
            
            if let error = error {
                print(error.localizedDescription)
                completion(error.localizedDescription)
            }
            else {
                if querySnapshot!.documents.isEmpty {
                    print("No data Found")
                    completion("Kitchen not found. Make sure you typed the code exactly. Capitalization matters.")
                } else {
                    print("Data found. ID assigned to KitchenManager.userKitchenID")
                    self.userKitchenID = id
                    completion(nil)
                }
            }
        }
    }
}

Notice this section that is added to my function’s definition:

completion: @escaping(_ errorMessage: String?) -> Void)

And this section when I was ready to pass information back to the viewController.

completion(error.localizedDescription)

Thank you, @mikaelacaron !

1 Like

Also another way to think of it is you’re calling the completion handler! Cause that’s what’s really happening when you write completion() you are then calling that completion block that’s written at the call site