swiftswiftuiobservable

ObservableObject + @Published vs objectWillChange.send()


I observed a strange pattern while using @ObservedObject and @Published.
I inject it inside a SwiftUI view but the view seems to refresh before the actual change of the object.

So my @ObservedObject doesn't have the right value when I check it's @Published property.

If I add a didSet with an objectWillChange.send() it works as expected.

Here is the code I'm working with:

class AuthManager: ObservableObject {
    @Published public private(set) var user: User {
        willSet { print("Will set") }
        didSet { print("Did set") }
    }
}


struct AuthView: View {
    @ObservedObject var authManager: AuthManager

    var body: some View {
        let _ = Self._printChanges()
        print("AuthManager user = \(authManager.user)")

        return (
            Text("AuthManager Test")
        )
    }
}

When I update my user object in the AuthManager, I get the logs in the following order:

Will set
AuthView: @self, @identity, _authManager
AuthManager user = xxx // <-- old version
Did set

The behavior works as expected if I remove the @Published and trigger the objectWillChange.send() in the didSet.

I'm expecting to have the new version when the view refresh itself. Am I missing something or is there something I don't understand about ObservableObject and @Published?

Thanks


Solution

  • The objectWillChange publisher of an ObservableObject always emits a new event in the willSet handler:

    A type of object with a publisher that emits before the object has changed.

    See the official documentation.

    The behavior you observed in this regard is therefore normal.

    If you change a value of a Published property, a new layout process is planned, but not executed directly. This enables all changes to be collected first and applied together in the next layout process.

    Take the following example, which is slightly modified from your code, and you will see that the output is correct as expected:

    @MainActor
    final class ContentViewModel: ObservableObject {
        @Published var myValue = ""
    }
    
    var counter = 0
    
    struct ContentView: View {
        @StateObject private var viewModel = ContentViewModel()
    
        var body: some View {
            let _ = Self._printChanges()
            let _ = print("myValue = \(viewModel.myValue)")
    
            VStack {
                Text(viewModel.myValue)
    
                Button("Change Value") {
                    counter += 1
                    viewModel.myValue = "Greetings \(counter)!"
                }
            }
            .padding()
        }
    }
    

    The output after pressing the button will be:

    ContentView: _viewModel changed.
    myValue = Greetings 1!
    

    What you don't show us in your code is how you change the user value of AuthManager.

    There are many things that can cause the problem you describe. For example, if you do not change the value from the main thread. Note how I did bind the ContentViewModel in my example to the MainActor.

    Your log outputs are also unusual. Changing the user property should not cause a change of the structural identity of the view, the only log output you should actually see is:

    AuthView: _authManager changed.
    

    @self and @identity should not be included in the log output.

    This output therefore indicates that you have a problem in the parent view , another surrounding view of AuthView or the way you're changing the Published property.