How do I update UILabels synchronously with Firestore data?

128 Views Asked by At

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

1

There are 1 best solutions below

2
Jay On BEST ANSWER

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)