Why SwiftUI @State property can be initialized inside `init` this OTHER way:

I would love if you can share the source. State and StateObject were both designed for being view private without injecting values from a parent view. If you want an observable object, use ObservedObject instead.

2 Likes

But there are cases where this is needed but these two PW are explicitly designed to not support this like any other PW?

I don't understand the question. Both are PWs and act in the same way (except StateObject because of the autoclosure). There are no compile time restrictions. There are just some semantical effect due to the design of SwiftUI. If one does not understand those fine semantics, one will run into bugs and unexpected behavior.

So whether this is a problem depends on how the view is coded. It may or may not be problematic.

You can find them in the transcripts of the digital lounges, such this one. Namely:


Original Question: When I’ve needed to inject data into a detail view, but still let the view have ownership of its StateObject, I’ve used the StateObject(wrappedValue:) initializer directly in my view initializer, for example:

public struct PlanDetailsView: View {

    @StateObject var model: PlanDetailsModel
    
    public init(plan: Plan) {
        self._model = StateObject(wrappedValue: PlanDetailsModel(plan: plan))
    }

    ...
}

Is this an acceptable use of the initializer? I know StateObject is only supposed to initialize at the start of the View’s lifetime, and not on subsequent instantiations of the View value, so I want to make sure I’m not forcing it to re-allocate new storage each time the View is re-instantiated.

Answer: Yes, this is an acceptable use of the initializer and your understanding is correct: that object will be create only at the beginning of the view lifetime and kept alive. The StateObject’s wrapped value is an autoclosure that is invoke only once at the beginning of the view lifetime. That also means that SwiftUI will capture the value of plan when is firstly created; something to keep in mind is that if you view identity doesn’t change but you pass a different plan SwiftUI will not notice that.

5 Likes

Correct. It you understand the effects of the opaque lifetime, there won't be any issues. A lot of people have wrong expectations with those two wrappers.

:point_up_2:This! It's not documented anywhere else. People have to find out it on their own. I understand exactly those effects and I personally have no issues running it like this where needed. It's unfortunate, but a lot of people seem to miss this and get confused why their views misbehave.

My understanding so far is:

These two syntax’s are equivalent. They both do the some to the @State/@StateObject property wrapper. It’s fine to do this as long as you know the view’s lifetime will not caused the view to be re-created: views can be re-created due to (why do view get re-created?) whatever.

I wish I know what the lifetime of a view is and how and why views have lifetime.

Yes, and there was a follow up to that question asking for the documentation to be updated.

3 Likes

There are a few ways to debug view lifetimes. You can add the "magic" line let _ = Self._printChanges() inside your view's body to see when and why it gets (re)evaluated. When the printed message says the identity changed, that's when any @State properties are captured to the view hierarchy and @StateObjects are lazy-initialised.

You can also get an idea of view identity by adding the wrapped property @Namespace var namespace to the view. Sometimes I prepend it to Self._printChanges() with any extra info about the view which helps me debug things like a view's title or URL or item ID, something like:

var body: some View {
  let _ = print(self.title, self.namespace, terminator: " -- ")
  let _ = Self._printChanges()
  ...
}

That little terminator helps keep the info on the same line.

When we talk about view lifetime, it's not the lifetime of the view struct but of a node in a view graph hidden inside SwiftUI's internals. AFAICT the @Namespace wrapped property refers to the ID of that node.

10 Likes

Thank you very much! This is the first time I see explanations of what the view lifetime is. Very nice and than you!

So far I’ve not needed to do any view lifetime debug. I just haven’t encounter any such problem so far.

I believe it's because StateObject applies private(set) modifer to its wrapped value. The code demonstrates the same behavior.

@propertyWrapper
struct DummyWrapper<T> {
    private(set) var wrappedValue: T

    public init(wrappedValue: T) {
        self.wrappedValue = wrappedValue
    }
}

struct Test {
    @DummyWrapper var x: Int

    init(x: Int) {
        // This works
        // _x = DummyWrapper(wrappedValue: x)

        // Error: cannot assign to property: 'x' is a get-only property
        self.x = x
    }
}

So I think the statement in SE-0528 @ole quoted above is true, as long as the wrapped property is settable in init(). That said, I also only use the underscore syntax because it's explicit and always works.


Note: as pointed by others above, just because PW initialization code compiles doesn't necessarily means it does what the user intends to do. But that's PW specific behavior.

1 Like

Yes, but that shouldn't be the case, should it? This is about the initialization of the property wrapper, so the setter shouldn't be involved at all.

This looks like a bug to me. The compiler is rejecting code that should be legal according to the out-of-line init rules in SE-0258. cc @hborla

@rayx Thank you for the code sample. It's good to have a self-contained example that doesn't rely on SwiftUI.

1 Like

I thought about it too. I think setter might be called if StateObject.init(wrappedValue:) is implemented by calling the wrappedValue setter.

I actually have the same question on @mayoff's explain here, where he assumes wrappedValue's setter is called in State(wrappedValue:).

1 Like

If I understand correctly, this is a different case because the property has already been initialized before init is run. Because Swift treats this:

@State var value: Int?

as if you had written this:

@State var value: Int? = nil

The property is already initialized when init runs, so it makes sense that init accesses the setter. But this is not the case in our example in this thread.

2 Likes

Ah, that makes sense. I agree with you that it seems to be a bug.

I agree this looks like a bug. I don't see a reason why self.x = x would be re-written to a setter call rather than re-written to property wrapper initialization. Please feel free to file an issue at Issues · apple/swift · GitHub!

4 Likes

That’s because the T? @State var sometimes are not auto initialized to nil and so later inside init the var can be assigned and you are saying this should be an init. Either ways, something odd is happening that allow a T? to init,table inside init. This should be allowed and you should use Optional as type.

See SwiftUI @State PW exact same code different result if another optional is added!? :=( - #11 by young

The case @ole pointed out does not use any optionals. That case looks like a compiler bug.

1 Like

I filed Out-of-line-initialized property wrapper incorrectly diagnosed as error if wrappedValue is read-only · Issue #63261 · apple/swift · GitHub.

6 Likes