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

struct Foo: View {
    @State private var flag: Bool
    
    init(flag: Bool) {
        _flag = .init(wrappedValue: flag)   // this is the conventional way to initialize @State in init
    }
        
    var body: some View {
        Text("Hello, world!")
    }

}


struct FooFoo: View {
    @State private var flag: Bool
    
    init(flag: Bool) {
        // but this work too and I want to know why and how State can do this and how to allow this for my own property wrapper
        self.flag = flag
    }
        
    var body: some View {
        Text("Hello, world!")
    }

}

This is documented in the proposal SE-0258 Property Wrappers:

Out-of-line initialization of properties with wrappers

A property that has a wrapper can be initialized after it is defined, either via the property itself (if the wrapper type has an init(wrappedValue:)) or via the synthesized storage property. For example:

@Lazy var x: Int // ... x = 17 // okay, treated as _x = .init(wrappedValue: 17)
3 Likes

This way of init'ing property wrapper is more straight forward and easier to read the the _ way. Wonder why no example of any SwiftUI @State deferred init using this better way?

Thanks for answering!

I can't speak for others, but speaking for myself, there are so many edge cases and rules about property wrapper initialization that I can't possibly remember them all, so I find it easier to just remember the general _flag = … rule.

This is getting into SwiftUI specifics, but as another example, the init syntax without the underscore doesn't work with @StateObject:

import SwiftUI

final class Model: ObservableObject {
    @Published var flag: Bool

    init(flag: Bool) {
        self.flag = flag
    }
}

struct Foo: View {
    @StateObject private var model: Model

    init(flag: Bool) {
        // Error: cannot assign to property: 'model' is a get-only property
        self.model = Model(flag: flag)
    }

    var body: some View {
        Text("Hello, world!")
    }
}

I'm not sure why because StateObject does have an init(wrappedValue:) initializer, so it seems to fulfill the requirement I quoted above. Is it because StateObject's initializer uses an @autoclosure? I don't know.

The error message "cannot assign to property: 'model' is a get-only property" seems to suggest that the compiler treats the line self.model = Model(flag: flag) not as an initialization, but as an assignment of a property that has already been initialized before. But where? I don't understand it?

1 Like

Oh, that’s confusing: so this neat way of init is not something capability intrinsic to any property wrapper? Some can break like @StsteObject

Just on the side note, unless you understand ALL semantics of State, don't initialize it from the init and pass some value from another view.

3 Likes

So you think initializing @State in init is a sign of mistake/wrong design?

I think I’ll just use the simpler form unless it doesn’t compile than in those case, revert back to _ way.

1 Like

Yes and no. If you understand the lifetime of a view. Then you'll understand what will happen to the new initialized state (btw. same goes for StateObject). The compiler cannot enforce that those PWs should be type private and not be initialized directly from an init, but those two are designed that way.

Normal non-PW private properties can be initialized in init. Why private PW should not do this same thing?

The issue is the lifetime of a particular view which is transparent. The framework discards any new values and restores everything that it cached.

I found an older test example that demonstrates the effects. Compile and test it for yourself. ;)

Again, this only affects State and StateObject, not other PWs.

This works fine for StateObject and is actually recommended.

It works but it is not recommended. You should not pass or capture an object from some parent view unless you understand all the effects. Most people unfortunately don't.

1 Like

It's explicitly recommended by the SwiftUI team (in the WWDC '21 labs) as a way to pass model objects down the view hierarchy.

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.