How do I rerender my view in swiftUI after a user logged in with Google on Firebase?

Viewed 2596

I'm fairly new to iOS Programming and swiftUI in particular. I have the issue that I try to integrate firebase auth in my App in order to manage users. Now the login, and log out basically works, the issue is, that after logging in, my view (which conditionally renders eighter the Google sign-in button or a list of content does not rerender so I still see the sign-in button even though I'm signed in).

I have set an observable Object to hold my auth status but unfortunately, it does not reload the current user automatically. So I set up a function to reload it manually which I would like to trigger when logging in. This works for the logout button but the logging in finishes in the AppDelegate, where for some reason I can't access the reloadUser() function.

I'm sure there is a better way to do this and would appreciate any help!

The Environment:

final class UserData: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var qrTags = qrTagData
    @Published var user: User? = Auth.auth().currentUser

    func reloadUser() -> Void {
        self.user = Auth.auth().currentUser
    }
}

The View I'd like to render:

struct MyQuaggsList: View {
        @EnvironmentObject private var userData: UserData

        var body: some View {
            Group {
                if getLogInState() != nil {
                    VStack {
                        NavigationView {
                            List {
                                Toggle(isOn: $userData.showFavoritesOnly) {
                                    Text("Show Favorites Only")
                                }

                                ForEach(userData.qrTags) { qrTag in
                                    if !self.userData.showFavoritesOnly || qrTag.isFavorite {
                                        NavigationLink(
                                            destination: QuagDetail(qrTag: qrTag)
                                                .environmentObject(self.userData)
                                        ) {
                                            QuaggRow(qrTag: qrTag)
                                        }
                                    }
                                }
                            }
                            .navigationBarTitle(Text("My Quaggs"))
                        }
                        SignOutButton()
                    }
                } else {
                    SignInView()
                }
            }.onAppear(perform: {self.userData.reloadUser()})
        }

        func getLogInState() -> User? {
            return Auth.auth().currentUser
        }
    }

Also, note there is the .onAppear() function which unfortunately only triggers on the initial appear not on the reappearance of the view after the user logged in.

Thanks so much in advance! It has been really frustrating.

2 Answers

The firebase and swiftUI combination is kinda tricky at first, but you will figure out that the same pattern is used in every single project, no worries. Just follow my steps and customise on your project, here is our strategy.

- This might be a long answer, but i want to leave it as a refrence to all Firebase-SwiftUI user Managing in Stack OverFlow. -

  1. Creating a SessionStore class which provides the BindableObject, and listen to your users Authentification and Handle the Auth and CRUD methods.
  2. Creating a Model to our project ( you already did it)
  3. Adding Auth methods in SessionStore Class.
  4. Listening for changes and putting things together.

Let s start by SessionStore Class: import SwiftUI import Firebase import Combine

class SessionStore : BindableObject {
    var didChange = PassthroughSubject<SessionStore, Never>()
    var session: User? { didSet { self.didChange.send(self) }}
    var handle: AuthStateDidChangeListenerHandle?

    func listen () {
        // monitor authentication changes using firebase
        handle = Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                // if we have a user, create a new user model
                print("Got user: \(user)")
                self.session = User(
                    uid: user.uid,
                    displayName: user.displayName
                )
            } else {
                // if we don't have a user, set our session to nil
                self.session = nil
            }
        }
    }

    // additional methods (sign up, sign in) will go here
}

Notice that we’ve declared that our session property is an optional User type, which we haven’t yet defined. Let’s quickly make one:

class User {
    var uid: String
    var email: String?
    var displayName: String?

    init(uid: String, displayName: String?, email: String?) {
        self.uid = uid
        self.email = email
        self.displayName = displayName
    }

}

Now, adding signUp, signIn and signOut methods

class SessionStore : BindableObject {

    // prev code...

    func signUp(
        email: String,
        password: String,
        handler: @escaping AuthDataResultCallback
        ) {
        Auth.auth().createUser(withEmail: email, password: password, completion: handler)
    }

    func signIn(
        email: String,
        password: String,
        handler: @escaping AuthDataResultCallback
        ) {
        Auth.auth().signIn(withEmail: email, password: password, completion: handler)
    }

    func signOut () -> Bool {
        do {
            try Auth.auth().signOut()
            self.session = nil
            return true
        } catch {
            return false
        }
    }
}

Finally, we need a way to stop listening to our authentication change handler.

class SessionStore : BindableObject {

    // prev code...

    func unbind () {
        if let handle = handle {
            Auth.auth().removeStateDidChangeListener(handle)
        }
    }
}

Finally, Making our content view:

import SwiftUI

struct ContentView : View {

  @EnvironmentObject var session: SessionStore
  var body: some View {
    Group { 
     if (session.session != nil) {  
      Text("Hello user!") 
     } else {
        Text("Our authentication screen goes here...")   
    }  
   }
  }
}

@Ghazi Tozri thanks again for your answer, while it wasn't what I wanted to do exactly it pushed me in the right direction. I just want to put here what I finally did so if anyone wants to not use Email / Password sign-in but Google sign-in they can benefit from it too.

I used the Combine Framework + the @Publisher Syntax to make it a bit more readable and I also don't need the Signing in and out Methods because Google Provides them.

The SwiftUI Button for Google sign-in would look something like this:

struct GoogleSignIn : UIViewRepresentable {
    @EnvironmentObject private var userData: SessionStore

    func makeUIView(context: UIViewRepresentableContext<GoogleSignIn>) -> GIDSignInButton {
        let button = GIDSignInButton()
        button.colorScheme = .dark
        GIDSignIn.sharedInstance()?.presentingViewController = UIApplication.shared.windows.last?.rootViewController

        //If you want to restore a session
        //GIDSignIn.sharedInstance()?.restorePreviousSignIn()
        return button
    }

    func updateUIView(_ uiView: GIDSignInButton, context: UIViewRepresentableContext<GoogleSignIn>) {
    }
}

And the Used SessionStore like this:

import SwiftUI
import Combine
import Firebase
import GoogleSignIn

final class SessionStore: ObservableObject {
    @Published var showFavoritesOnly = false
    @Published var qrTags = qrTagData
    @Published var session: User?
    var handle: AuthStateDidChangeListenerHandle?

    func listen() {
        handle =  Auth.auth().addStateDidChangeListener { (auth, user) in
            if let user = user {
                self.session = user
            } else {
                self.session = nil
            }
        }
    }
}

In a view that checks for the authentication state I use the .onAppear() function like this:

struct UserProfile: View {
    @EnvironmentObject private var session: SessionStore

    var body: some View {
        VStack {
            if session.session != nil {
                SignOutButton()
            } else {
                SignInView()
            }
        }.onAppear(perform: {self.session.listen()}) 
    }
}

To sign out this function will do (from the Firebase Docs):

func signOut() -> Void {
        let firebaseAuth = Auth.auth()
        do {
            try firebaseAuth.signOut()
        }
        catch let signOutError as NSError {
            print ("Error signing out: %@", signOutError)
        }
    }

Hope this can help somebody else too.

Related