Search code examples
iosswiftxcodeswiftui

SwiftUI .onAppear wont trigger when @Environment changes


I'm trying to convert all my @EnvironmentObject to @Environment and I ran into an issue. The UserService has a property user which is nil. After that a network call takes place and the user gets updated. These changes get published in the View but unlike before, .onAppear() is not triggered again. I need this because once I have the user I need to call a method in the ViewModel which needs the user.

This used to work fine before but I guess something under the hood must have changed. Is there a new way to handle this?

@Observable class UserService {
    var user: User?
    
    var handle: AuthStateDidChangeListenerHandle?
    
    init() {
        print("UserService int()")
    }
    
    func listen() {
        handle = Auth.auth().addStateDidChangeListener({ (auth, user) in
            self.getCurrentUser(uid: user?.uid)
        })
    }
    
    func getCurrentUser(uid: String?) {
        guard let uid = uid else { return }
        Firestore.firestore().collection("users").document(uid).getDocument { documentSnapshot, error in
            do {
                self.user = try documentSnapshot?.data(as: User.self)
                print("getCurrentUser", self.user?.displayName)
            } catch {
                print("error")
                self.error = error.localizedDescription
            }
        }
    }
}

struct DiscoverView: View {
    @Environment(UserService.self) private var userService
    
    var body: some View {
        // OK gets updated when user is updated
        Text(userService.user?.displayName ?? "nil")
            .onAppear() {
                print("onAppear")
                if userService?.user {
                    //trigger something else 
                }
                // will trigger once and wont trigger when user is updated
            }
    }
}

Solution

  • The fact that onAppear is only called once in your case is actually the correct behavior and how it should be.

    Only if the (structural) identity of a SwiftUI view were to change would onAppear also be called multiple times. A change to a state of a view does not result in multiple calls to onAppear.

    To observe the change of User in your case, the correct way would be to use onChange:

    Text(userService.user?.displayName ?? "nil")
        .onChange(of: userService.user) { oldUser, newUser in
            print("User changed: \(newUser?.displayName)")
        }
        .onAppear() {
            print("onAppear")
        }
    

    If onAppear was called multiple times in your old code, this could actually indicate a problem with your view hierarchies and the SwiftUI state handling. The use of conditional view modifiers, for example, can lead to this issue.