@StateObject, and @ObservedObject, usage questions

As an experienced C++ developer who is on the Swift learning curve I am somewhat confused by the semantics of the @StateObject, and @ObservedObject, wrappers. I understand the concept of the source of truth. But when I see an an example such as this one:

I see a Player class instantiation within the ContentView's scope with the object name "player", and a declaration of an object named "player" within the PlayerNameView's scope. What makes the connection between the two scopes to cause both player objects in the two different scopes reference the same data?

Does the @StateObject keyword make the object named "player" add something akin to a static object to the class which would make the data belong to the class instead of an object instantiated from that class? Or does it make something like a database for the application?

In order for the connection to be made must the object name, which in this case it is "player", be the same in both scopes?

Is @StateObject, and @ObservedObject, usage only valid within an object that conforms to the View protocol?

See the PlayerNameView declaration inside the ContentView? This is an anonymous instantiation. There no magic with the name "player", it's just that it's the right type, and being passed into the PlayNameView instantiation.

As Hacksaw said, there is no magic here. The two variables points to the same object due to this line in the code:

PlayerNameView(player: player)

No, that's not true. The "State" in StateObject means the object's life cycle is associated with a view. It's instantiated when that view is created, and destroyed when that view is gone.

A very tricky part: a view may be instantiated many times but with the same id. In that case the StateObject is only instantiated once. The key to understand the behavior: a) think view as view description, instead of actual widget, b) the reason why StackObject is instantiated just once is an implementation details specific to that property wrapper.

(Feel free to ignore the tricky part if you find it's confusing).

The code would compile but doesn't actually work. SwiftUI property wrappers are designed to work with view hierarchy maintained internally in SwiftUI. That's why they don't work in non-UI code.

Thank you, both of you, for your answer. All is clear to me now. I feel foolish for not examining closely enough the entire sample code. I failed to notice the line:

 PlayerNameView(player: player)
1 Like

Another question about that example code. The Player class is instantiated in the ContentView. Does that mean a new player object is created every time the ContentView does its update?

No. When ContentView updates, its body, not its init(), gets called, so the Player isn't instantiated.

But I don't think that's your real question. I guess your question is: if a view's init() is called multiple times (for example, because its parent view's body gets called), does its state object gets instantiated every time? The answer is also no, because a state object's life cycle is associated with the view. It's only recreated/destroyed when that view is recreated/destroyed.

How is that implemented? It's because StateObject's wrappedVallue is a closure:

@inlinable public init(wrappedValue thunk: @autoclosure @escaping () -> ObjectType)

So Player isn't instantiated by the compiler during StateObject initialization. It's up to SwiftUI to determine if the closure should be called.

1 Like

Indeed, and it could be quite surprising as the following minimal app shows:

import SwiftUI

class Model: ObservableObject {
    @Published var parameter = 1
    init(parameter: Int) {
        print("Model.init called with parameter: \(parameter)")
        self.parameter = parameter
    }
}

struct SubView: View {
    @StateObject private var model: Model
    
    init(parameter: Int) {
        print("SubView.init called with parameter: \(parameter)")
        _model = StateObject(wrappedValue: Model(parameter: parameter))
    }
    var body: some View {
        Text("Hello \(model.parameter)")
    }
}

struct ContentView: View {
    @State private var parameter = 1
    
    var body: some View {
        SubView(parameter: parameter)
            .onTapGesture {
                parameter += 1
            }
    }
}

@main struct SUI8App: App {
    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

Here on each tap you'll see that SubView init is called with a new parameter but the corresponding Model.init is not getting called and the model parameter is not changed. If you change it to a seemingly equivalent sequence, calling model initialiser out of line:

init(parameter: Int) {
    print("SubView.init called with parameter: \(parameter)")
    let theModel = Model(parameter: parameter)
    _model = StateObject(wrappedValue: theModel)
}

the model initialiser is getting called but then the resulting value is thrown away anyway.

Then, if you change the model to be ObservedObject and pass it from outside as a parameter:

struct SubView: View {
    @ObservedObject var model: Model
    var body: some View {
        Text("Hello \(model.parameter)")
    }
}

struct ContentView: View {
    @State private var parameter = 1
    
    var body: some View {
        SubView(model: Model(parameter: parameter))
            .onTapGesture {
                parameter += 1
            }
    }
}

It works as the doctor ordered.

2 Likes

That's why, as much as I like SwiftUI as a UI design tool, I don't like it as a language. I mean, with the heavy use of result builder and property wrapper, it differs from the plain old Swift code so much and violates the least surprice principle rampantly. I'm lucky that I had a moderate knowledge of Swift before I used SwiftUI. But I always wonder how those new beginners who dive in SwiftUI can survive. Even worse, they might think the mysterious behaviors they observed are just the way how Swift works.

2 Likes

That's why we are discussing the new Observation machinery.

1 Like

Isn't it mainly a replacement of Combine's ObservableObject? How will it help in this case?

The change to ObservableObject / Published / ObservedObject will highly likely affect neighbouring areas, e.g. how StateObject is getting initialised (and whether it exists at all and not changed for something else).

Last I saw it would allow for the complete removal of those property wrappers altogether. Instead the SwiftUI runtime would simply observe any Observable state accessed in the body automatically.

Just to be clear, in this case it works “incorrectly, as expected”.

Creating ObservedObjects inside the body is a very fundamental mistake in unsung SwiftUI, that I’m struggling to educate my colleagues about.

Is there a linter to detect this?

I was also thinking to make a debug wrapper view which calls body twice and checks that results are identical using the same functions from AttributedGraph that SwiftUI uses. But didn’t try it yet. Did someone else try? WDYT of idea in general?

Regarding the new observation - it will eliminate need for @Published and @ObservedObject, but not for @State and @StateObject. It won’t even allow to replace usage of @StateObject with @State, because the former has lazy initializer, while the latter has an eager one.

But it would allow to automatically observe objects stored as optional in @State.

2 Likes

I was thinking about a similar idea of doing view testing via comparison the view (the result of a body call) to what it should be. There were quite a few obstacles, mostly due to over-opaqueness (by design) of SwiftUI structures.

Wait what?! Where can I learn more about this observable change?

Because what I’m reading here sounds like it’ll destroy the way I use ObservableObjects. Often I’ll only publish some vars and leave others unpublished. In the app I’m working on now if all properties are automatically published it won’t work, the app will crash. And sometimes I’ll hold onto an ObservableObject in a view without using the ObservedObject tag because I don’t want it triggering that view to redraw but I do want to pass it to other views or change a value, but not observe.

I like using Published and ObservedObject because it gives me control over data flow and view updates and I’m worried this change (whatever it is) will make half my apps slower or just straight up crash :scream: Please tell me the sky isn’t falling!

It's still being designed, see the relevant thread. You'd just have to split your state into two parts - "signalling" and "not signalling". Not a big deal, or is it?

1 Like

Many thanks tera :heart: I’ve had a little time to look it over and while I’m not advanced enough to follow all of it it looks like just a new feature to the language, not a replacement for ObservableObject. Of course what Apple does with SwiftUI is their deal. I’ll read more of the proposal later, it sounds interesting, but my fret is over for now, whew.

If/when SwiftUI is switched to the new machinery the old way would still be available for quite some time, even if in a deprecated form, and then removed a few years afterwards, so I wouldn't be worry now; the new way itself doesn't even exist yet, neither the new SwiftUI implementation that is using it.

1 Like

Could you elaborate a bit on this? I never read about StateObject having a lazy initializer. Or do you mean the use of autoclosure, instead of the language's lazy keyword?

Thanks. I read through that thread. My understanding is that the new observation mechanism doesn't deal with object's life cycle, so State and StateObject are still required.