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

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