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
}
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). Removereturn 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()
}
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:
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