swiftfirebasegoogle-cloud-platformgoogle-cloud-firestorefirebase-authentication

How can I load user data from Firestore and update my view after authenticating asynchronously in Swift?


I'm looking for a way to get a user from my Firestore db AFTER authentication is completed and I get the UID for the user but BEFORE my view loads on SwiftUI/Swift 5+. Or alternatively, I can have a ProgressView in between.

In my ContentView view, I have a conditional clause on whether a user is onboarded or not. This is a field in my User document on Firebase that I need to access.

The problem I'm running into is that when my view loads, it loads with the default values of my User struct when I need the values from the database.

Here's my solution right now:

I made a SessionStore class to handle the logins with Firebase:

class SessionStore : ObservableObject {
  u/Published var session: SessionUser? { didset { self.didChange.send(self) } }
  var handle: AuthStateDidChangeListenerHandle?

  // Auth listening by running addStateDidChangeListener
  func listen() { ... }

  // Unbind the Auth listener
  func unbind() { ... }

  func handleGoogleLogin() {
    GIDSignIn.sharedInstance.signIn(withPresenting: getRootViewController()) {
              signInResult, err in
    ...
    
      signInResult.user.refreshTokensIfNeeded { user, error in
      ...

        Auth.auth().signIn(with: credential) { result, err in
          print("success!")
          // TODO: run fetch user here somehow
        }
      }
    }
  }
}

Then in my SessionUser object, I have the call to fetch data from the collection on Firestore:

class SessionUser : ObservableObject {
  @Published var user: User?
  @Published var isUserOnboarded: Bool
  var uid: String
  
  init(uid) { ... }

  func fetchSessionUser() async {
    do {
      // Tries to get user from the datastore first in case user exists already
      if doesUserExist() {
        // Get user from Firestore db
        let doc = try await db.collection("users").document("\(userId)")
        if doc.exists {
          self.user = try document.data(as: User.self)
          self.isUserOnboarded = self.user.isUserOnboarded
        }
      } else {
        // Create user
      }
    }
  }
}

For posterity, here's the ContentView view:

struct ContentView: View {
  @StateObject var session: SessionStore = SessionStore()

  var body: some View {
    VStack {
      if session.session != nil || Auth.auth().currentUser != nil {
        if session.session?.isUserOnboarded {
          MainView()
        } else {
          OnboardingView()
        }
      } else {
        StartPageView()
      }
   }
}

I've tried a lot of things such as escaping the object in my fetch call, creating an encapsulated class with a published variable for the isUserOnboarded (as shown above), and playing around with DispatchQueue.main.async calls, but nothing has worked so far.

Previous answers on StackOverflow/online show the Auth procedure for Firebase as being async (but it's not anymore I guess) and it doesn't allow for me to call it with an await.


Solution

  • I ended up switching to a snapshot listener to avoid the await call and then also changed my SessionUser to have an @Observable macro instead of inheriting the ObservableObject since there were issues with nested classes not updating the view with my @Published variables.