swiftfirebasegoogle-cloud-firestoreswrevealviewcontroller

How do I update UILabels synchronously with Firestore data?


I'm currently building an iOS app that will synchronize account information from Firestore. I have the login/register process hooked up and working. However, I need help understanding how to update my logInOutBtn, fullNameTxt and emailTxt in my MenuVC automatically when an user logs in/out. Currently, it will update whenever I close then reopen the menu, but what should I use to automatically update it without having to close the menu? Thanks!

// MenuVC

override func viewDidAppear(_ animated: Bool) {

        if let user = Auth.auth().currentUser , !user.isAnonymous {
            // We are logged in
            logInOutBtn.setTitle("Logout", for: .normal)
            if UserService.userListener == nil {
                UserService.getCurrentUser {
                    self.fullNameTxt.text = UserService.user.fullName
                    self.emailTxt.text = UserService.user.email
                }
            }
        } else {
            logInOutBtn.setTitle("Login", for: .normal)
            self.fullNameTxt.text = "Sign in or create an account"
            self.emailTxt.text = "to continue."
        }
    }

fileprivate func presentLoginController() {

        let storyboard = UIStoryboard(name: Storyboard.LoginStoryboard, bundle: nil)
        if #available(iOS 13.0, *) {
            let controller = storyboard.instantiateViewController(identifier: StoryboardId.LoginVC)
            present(controller, animated: true, completion: nil)
        } else {
            // Fallback on earlier versions
        }

    }

@IBAction func logInOutClicked(_ sender: Any) {

        guard let user = Auth.auth().currentUser else { return }

            if user.isAnonymous {
                presentLoginController()
            } else {
                do {
                    try Auth.auth().signOut()
                    UserService.logoutUser()
                    Auth.auth().signInAnonymously { (result, error) in
                        if let error = error {
                            debugPrint(error)
                            Auth.auth().handleFireAuthError(error: error, vc: self)
                        }
                        self.presentLoginController()
                    }
                } catch {
                    debugPrint(error)
                    Auth.auth().handleFireAuthError(error: error, vc: self)
                }
            }
        }

// UserService

func getCurrentUser(completion: @escaping () -> ()) {

        guard let authUser = auth.currentUser else { return }
        let userRef = db.collection("users").document(authUser.uid)

        userListener = userRef.addSnapshotListener({ (snap, error) in

            if let error = error {
                debugPrint(error.localizedDescription)
                return
            }

            guard let data = snap?.data() else { return }
            self.user = User.init(data: data)
            completion()
        })

// User Model

struct User {

    var fullName: String
    var address: String
    var id: String
    var email: String
    var stripeId: String

    init(fullName: String = "",
         address: String = "",
         id: String = "",
         email: String = "",
         stripeId: String = "") {

        self.fullName = fullName
        self.address = address
        self.id = id
        self.email = email
        self.stripeId = stripeId
    }

    init(data: [String : Any]) {
        fullName = data["fullName"] as? String ?? ""
        address = data["address"] as? String ?? ""
        id = data["id"] as? String ?? ""
        email = data["email"] as? String ?? ""
        stripeId = data["stripeId"] as? String ?? ""
    }

    static func modelToData(user: User) -> [String : Any] {

        let data : [String : Any] = [
            "fullName" : user.fullName,
            "address" : user.address,
            "id" : user.id,
            "email" : user.email,
            "stripeId" : user.stripeId
        ]

        return data
    }

}

// My app menu

enter image description here


Solution

  • The signout process is pretty straightforward and is marked as throws so if it fails, it will generate an error that can be handled by a catch. It is not asynchronous so it won't have (or need) a closure.

    So simply stated

    func signOut() {
    
        let firebaseAuth = Auth.auth()
    
        do {
            try firebaseAuth.signOut()
            print("successful signout")
            self.logInOutBtn.setTitle("Log In", for: .normal)
            self.fullNameTxt.text = ""
            self.emailTxt.text = ""
    
        } catch let signOutError as NSError {
            print ("Error signing out: %@", signOutError)
            //present the error to the user/handle the error
        }
    }
    

    The signIn function is asynchronous with a closure so when the user signs in successfully, the code in the closure will fire and that's the perfect place to update the UI.

    Auth.auth().signIn(withEmail: email, password: password) { [weak self] authResult, error in
      guard let strongSelf = self else { return }
      // update the UI here.
    }
    

    You can also just monitor the authState with an observer and have it react to users logging in/out

    self.authListener = Auth.auth()?.addAuthStateDidChangeListener { auth, user in
       if let theUser = user {
          print("User logged in \(theUser)") // User is signed in.
          self.dismissViewControllerAnimated(true, completion: nil)
       } else {
          print("Need to login.") // No user is signed in.
          //present login view controller
       }
    }
    

    If you no longer want to observe the auth state, you can remove it with

    Auth.auth()?.removeAuthStateDidChangeListener(self.authListener)