简体   繁体   中英

SwiftUI view is not updating to EnvironmentObject change

I'm creating a SwiftUI app that includes Firebase to enable logging into an account, extremely simple, just a ui form with password and email fields, then a button to submit. Once the user signs in I store the firebase user object in an EnvironmentObject so the rest of the views will have access to it. The problem with the app currently is that once the user logs in and the user data is stored in the EnvironmentObject, the view is supposed to update to the changed state of this to show a different screen, but it seems the view still thinks the EnvironmentObject is equal to nil. Do views not automatically change to updates in an EnvironmentObject like they do for state variables perhaps?

I've made sure the EnvironmentObject is setup properly and passed to both the preview and SceneDelegate

Made sure that the app is indeed successfully logging in the user by printing account information to the console upon sign in, yet the view itself will only display nil for account information, it seems it wont access the updated EnvironmentObject with the user info.

import SwiftUI
import Firebase
import Combine

struct ContentView: View {

    @EnvironmentObject var session: SessionStore

    @State var emailTextField: String = ""
    @State var passwordTextField: String = ""

    @State var loading = false
    @State var error = false

    var body: some View {
        VStack {
            if (session.session != nil) {
                Home()
            } else {
                Form {
                    TextField("Email", text: $emailTextField)
                    SecureField("Password", text: $passwordTextField)
                    Button(action: signIn) {
                        Text("Sign in")
                    }
                }

                Text("Session: \(session.session?.email ?? "no user")")
            }
        }.onAppear(perform: getUser)
    }

    func getUser () {
        session.listen()
    }

    func signIn () {
        loading = true
        error = false
        session.signIn(email: emailTextField, password: passwordTextField) { (result, error) in
            self.loading = false
            if error != nil {
                self.error = true
            } else {
                self.emailTextField = ""
                self.passwordTextField = ""
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView().environmentObject(SessionStore())
    }
}



class SessionStore : ObservableObject {

    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 account = user {
                // if we have a user, create a new user model
                print("Got user: \(account)")
                self.session = User(
                    uid: account.uid,
                    displayName: account.displayName,
                    email: account.email
                )
                print("Session: \(self.session?.email ?? "no user")")
            } else {
                // if we don't have a user, set our session to nil
                self.session = nil
            }
        }
    }

    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
        }
    }

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

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
    }

}

As you can see in the view, it is supposed to render login fields when user is not logged in, and when the user is logged in the view should display another view. That other view is not displaying.

Try to make use of the @Published property. Try to implement something like this:

class SessionStore : ObservableObject {
    @Published var session: User
}

class User: ObservableObject {
    @Published var uid: String
    @Published var email: String?
    @Published var displayName: String?

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

}

This should update your view when a change was made in the User object, like the email or displayname because they're Published. Hope this will help, gl

UPDATED:

Because SwiftUI doesn't support nested Observables yet, you need to notify your main model by yourself.

See this snippet how to work with a nested ObservableObject inside a ObservableObject:

class Submodel1: ObservableObject {
  @Published var count = 0
}

class Submodel2: ObservableObject {
  @Published var count = 0
}

class Model: ObservableObject {
  @Published var submodel1: Submodel1 = Submodel1()
  @Published var submodel2: Submodel2 = Submodel2()

    var anyCancellable: AnyCancellable? = nil
    var anyCancellable2: AnyCancellable? = nil

    init() {

        anyCancellable = submodel1.objectWillChange.sink { (_) in
            self.objectWillChange.send()
        }

        anyCancellable2 = submodel2.objectWillChange.sink { (_) in
            self.objectWillChange.send()
        }
    }
}

When data inside a submodel changes, the main Model will notify itself. This will result in a update on the view.

Let me know if this helped you out.. Goodluck!

In practice this previous solution works, but is a very simplistic a no realistic case scenario. What happens when you want to perform tab switch in a very deep view??

By using EnvironmentKey you can take current tab status from any View and change it from there. I will not write an example because is properly explained here: https://www.thirdrocktechkno.com/blog/using-swiftui-environments-values-to-change-tab/

Think you are mixing up ObservableObject with BindableObject ? Might try this instead:

@Published var session: User?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM