1

For some reason my firebase methods do things that make no sense to me. I want to login with a user and afterwards check on some data to make a decision. Both methods signIn() and getDocument() don't go beyond the curly brackets. If I set a breakpoint or step over the next point where it stops is outside the curly brackets. What am I doing wrong?

This is the whole code:

import Foundation
import Firebase

//@objc(LoginViewController)
class LoginViewController: UIViewController {

    @IBOutlet weak var errorMessage: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBOutlet weak var emailField: UITextField!
    @IBOutlet weak var passwordField: UITextField!

    @IBAction func didTapEmailLogin(_ sender: UIButton) {

        // Check if empty
        guard emailField.text != nil, passwordField.text != nil else {
            self.errorMessage.text = "Fields can't be empty."
            return
        }

        // Log in
        let email = emailField.text!
        let password = passwordField.text!
        guard login(with: email, with: password) else {
            print("Login didn't work")
            return
        }

        // Check if user has a group yet
        guard userHasGroup() else {
            print("Getting data didn't work")
            return
        }
    }

    func userHasGroup() -> Bool {
        var succesful = true
        let db = Firestore.firestore()
        let userUid = Auth.auth().currentUser?.uid
        let docRef = db.collection("users").document(userUid!)
        docRef.getDocument { (document, _) in
            if let document = document, document.exists {
                // Test
                print(document.data() as! [String: Any])
            } else {
                succesful = false
            }
        }
        return succesful
    }

    func login(with email: String, with password: String) -> Bool {
        var succesful = true
        Auth.auth().signIn(withEmail: email, password: password) { (user, error) in
            guard error == nil, user != nil else {
                // There was an error.
                self.errorMessage.text = "Email/password incorrect."
                succesful = false
                return
            }
        }
        return succesful
    }
}
Marcel Braasch
  • 1,083
  • 1
  • 10
  • 19
  • Did you report the same problem yesterday in [this question](https://stackoverflow.com/questions/57649466/get-documents-in-firebase-wont-even-jump-into-method), and then said it was gone after a reinstalled the pods? – Frank van Puffelen Aug 27 '19 at 03:47
  • Yes I did, because I thought it solved the problem.. but this keeps happening. – Marcel Braasch Aug 27 '19 at 03:52
  • So I just tried this: 1) I'm trying to sign in 2) program is not jumping inside the method 3) then program goes till the end of my didTapFunction 4) randomly jumps to my sign in function again 5) jumps inside method. (actually Xcode just crashed when trying to reproduce this ..) – Marcel Braasch Aug 27 '19 at 03:55
  • I'm updating my code so you can see the whole file. Maybe you can see what I'm doing wrong here.. – Marcel Braasch Aug 27 '19 at 04:00
  • My guess is that you're seeing asynchronous behavior, which is why I told you to put a breakpoint on the first line inside the callback/completion handler (`if let document =...`), and let it run. If that leads to the breakpoint being hit, it's expected behavior. – Frank van Puffelen Aug 27 '19 at 04:08
  • I wouldn't tell you it's not going inside if I didn't set a breakpoint inside which is not being hit ... like I said, the behavior I'm observing right now looks like this: 1) program starts 2) hits `signIn` line 3) does not go inside 4) executes the rest of the code in the function 5) iPhone simulator is tweaking 6) hits `signIn` again and goes inside this time. – Marcel Braasch Aug 27 '19 at 04:20
  • 1
    `Auth.auth().signIn` is an asynchronous task and will take some time to execute. You can not make it return type, use call back instead. – TheTiger Aug 27 '19 at 07:16
  • Thank you @FrankvanPuffelen. Didn't get the hint.. it was indeed an asynchronous thing.. I updated my solution. – Marcel Braasch Aug 27 '19 at 14:20
  • The updated code will not work, the `completionHandler` call in `userHasGroupAsync` must be **inside** the closure. – vadian Aug 27 '19 at 14:26
  • Yea you're right. I updated that :-) – Marcel Braasch Aug 27 '19 at 14:28
  • Please post your solution as an answer, instead of including it in the question. Self answers are quite acceptable here on Stack Overflow, and actually very much appreciated when they contain code like yours does. – Frank van Puffelen Aug 27 '19 at 18:54
  • Did as you said. Thanks Frank. – Marcel Braasch Aug 30 '19 at 00:00

2 Answers2

3

This is the definition of async behaviour. The first time through your function all the code outside the closure is executed. Then when this async call to signIn returns the code inside the closure is executed.

The problem is the structure of your function. You can't reliably return a value from a function that contains a closure as the value won't be set when the function returns.

You need to change your function to use a completion handler.

I've posted a recent example of how to do this here Why aren't my Firebase Storage URLs uploading to Google Cloud Firestore?

Gary M
  • 428
  • 5
  • 13
  • Thank you! Like Frank said, this seems like an asynchronous problem. I didn't know this is how you handle that in swift .. I used this blog series to get it working http://www.programmingios.net/what-asynchronous-means/ . I updated my code above, could you check if this is okay? – Marcel Braasch Aug 27 '19 at 14:17
  • While that is about async behaviour it's not the best example for what you are trying to do here. What you want is examples of completion handlers, also known as callbacks. This is a reasonable explanation https://medium.com/@nimjea/completion-handler-in-swift-4-2-671f12d33178 – Gary M Aug 27 '19 at 15:59
  • As for your code, loginAsync isn't quite right. You aren't handling errors. It would be better to have a variable inside the closure or explicitly call completionHandler(false) when an error occurs, instead of just return. The other function has the completionHandler outside the closure so that wont work. – Gary M Aug 27 '19 at 16:03
  • I dont unterstand why? When an error occurs im setting `successful`to false. Isn't that the same thing as what you said? and yeah I already fixed that other thing. – Marcel Braasch Aug 27 '19 at 18:28
  • 1
    No because it hits "return" before the completion handler. – Gary M Aug 28 '19 at 03:32
1

So indeed it was an async problem.

This is how I made it work:

  @IBAction func didTapEmailLogin(_ sender: UIButton) {

    // Check if empty
    guard emailField.text != nil, passwordField.text != nil else {
        self.errorMessage.text = "Fields can't be empty."
        return
    }

    let email = emailField.text!
    let password = passwordField.text!

    loginAsync(with: email, with: password) { (loginSuccesful) in
        if loginSuccesful {
            self.userHasGroupAsync(completionHandler: { (hasGroup) in
                if hasGroup {
                    self.performSegue(withIdentifier: "fromLoginToHome", sender: self)
                } else {
                    self.performSegue(withIdentifier: "fromLoginToCreateJoinGroup", sender: self)
                }
            })
        }
    }
}

func loginAsync(with email: String, with password: String, completionHandler: @escaping (Bool) -> ()) {
    var succesful = true
    Auth.auth().signIn(withEmail: email, password: password) {
        (user, error) in
        guard error == nil, user != nil else {
            // There was an error.
            self.errorMessage.text = "Email/password incorrect."
            succesful = false
            return
        }
        completionHandler(succesful)
    }
}

func userHasGroupAsync(completionHandler: @escaping (Bool) -> ()) {
    var hasGroup = false
    let db = Firestore.firestore()
    let userUid = Auth.auth().currentUser?.uid
    let docRef = db.collection("users").document(userUid!)
    docRef.getDocument { (document, _) in
        if let document = document, document.exists {
            let data: [String: Any] = document.data()!
            let group = data["group"] as! String
            if group != "" { hasGroup = true }
        }
        completionHandler(hasGroup)
    } 
}
Marcel Braasch
  • 1,083
  • 1
  • 10
  • 19