Assignment to @State var in init() doesn't do anything, but the compiler gen'ed one works

The following example summarize what I learned from this long thread. Hopefully this provide quick answer to anyone looking at this in the future. Please read @DevAndArtist's and @Lantua's comment in this thread for authoritative explanations.

// How to do View @State var initialization and what problem can arise from doing so...

import SwiftUI

struct Child: View {
    // How to hand code a init to initialized this?
    @State var blah: Int
    
    // For some reason you want to write your init instead of using the compiler synthesisized one
    init(blah: Int) {
        // this does not work:
        // self.blah = blah
        // "due to an artificial limitation in the compiler's implementation of property wrappers", see this bug report:
        // https://bugs.swift.org/browse/SR-12202?focusedCommentId=55108&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-55108
        // instead do this:
        self._blah = State(wrappedValue: blah)
        // But View @State var should not be initialized like this, its initial value can only be set once
        // there after, setting it in init has not effect. Same problem with compiler synthesized init since it's the same code gen'ed
        // however, you can overcome this by giving the view a new id
        // this basically sum up this whole thread (I hope)
    }
    
    var body: some View {
        Text("blah = \(blah)")
            .padding()
            .background(Color.yellow)
            .padding(.bottom)
    }
}


struct ContentView: View {
    @State var value = 1
        
    var body: some View {
        VStack {
            Text("This Child view @State var is stuck at 1 because @State caching")
                .multilineTextAlignment(.center)
            Child(blah: value)
            
            Text("This Child view gets every new value because its id change each time")
                .multilineTextAlignment(.center)
            Child(blah: value)
                .id(value)
            
            Stepper("Change value (\(self.value)):", value: self.$value)
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

==========================
Original post

This is my simplified example: I need to assign something to @State var value on init. Calling the compiler gen'ed one works, but my hand written init() doesn't:

import SwiftUI

fileprivate struct MyWidget: View {
    @State var value = -1       // need to override this in init from some other param
    
    var body: some View {
        Text("\(value)")
    }
}

// This is simplified example of what I needed to do:
// manually assigned something to value but this doesn't work!!
extension MyWidget {
    init(blah: Int) {
        self.value = blah   // Why this does nothing, still  display -1?
//        self.$value = blah    // nope, not this way
//        self._value = blah    // not this either
    }
}

struct InitAtStateVarMystery: View {
    var body: some View {
        VStack {
            MyWidget(value: 4444)       // this one works: display 4444
            MyWidget(blah: 8888)        // this one not: display -1
        }
    }
}

struct InitAtStateVarMystery_Previews: PreviewProvider {
    static var previews: some View {
        InitAtStateVarMystery()
    }
}

I need to assign to value in the init() from another Binding parameter. The code shown above is simplified.

Maybe you can call the synthesized init as a work around.

1 Like

:+1: Works!

Also, it should be this:

init(blah: Int) {
  _value = State(wrappedValue: blah)
}

since _value is the actual storage, and is of type State<Int>. That's why your commented code wouldn't even compile.

2 Likes

:pray::bowing_man:t3:

Now I know what the synth'ed init does to the @State var

Personally, I suggest to not touch a State in your init, even if by recreating the wrapper. If you need an initial value injected, I would go for a Binding instead

1 Like

I agree with @DeFrenZ. There's still certain amount of magic, like how it can distinguish between multiple States during init (I suspect it's the init order, but I wouldn't want to rely on that).

After thinking for a while, I believe the best way is to just call the synthesized init, and not even mutate the state during this time.

PS

If you don't know the initial value, you can just leave it to the parents to supply it:

fileprivate struct MyWidget: View {
    @State var value: Int // No need for value right now
    
    ...
}
1 Like

I want to allow cancel for my editing view. So instead of updating the Binding directly which would commit the change immediately, it uses an @State var draft that's initialized from the passed in binding:

import SwiftUI

final class DataStore: ObservableObject {
    @Published var scores = [4444, 123]
}

    
struct BindingDraftTrouble: View {
    @ObservedObject var data = DataStore()
    
    var body: some View {
        NavigationView {
            List {
                ForEach(0..<data.scores.count, id: \.self) { index in
                    NavigationLink(destination: EditingNumber(score: self.$data.scores[index])) {
                        Text("     \(self.data.scores[index])")
                    }
                }
            }.navigationBarTitle(Text("Scores"))
        }
    }
}

/// Edit on a draft copy
/// Only commit the change to the Binding when "Save" is pressed
/// Pressing back button is "Cancel"
fileprivate struct EditingNumber: View {
    @Environment(\.presentationMode) var presentation
    @Binding var score: Int
    @State var draft: Int

    init(score: Binding<Int>) {
        self._score = score
        // copy from passed in binding to use for editing
        self._draft = State(wrappedValue: score.wrappedValue)
    }
    
    var body: some View {
        VStack {
            Stepper(value: self.$draft) {
                Text("Score: \(draft)")
            }
            Button("Save") {
                self.score = self.draft     // commit the change
                self.presentation.wrappedValue.dismiss()
            }
        }
        .navigationBarTitle(Text("Edit"))
    }
}

struct BindingDraftTrouble_Previews: PreviewProvider {
    static var previews: some View {
        BindingDraftTrouble()
    }
}

Is there better way for this so I an avoid assigning to @State in my init?

what do you mean?

If you have multiple State in a view, SwiftUI somehow can distinguish between them, which is quite interesting when you think about it. There is definitely some underlying convention there.

struct Foo: View {
  @State var a: Int
  @State var b: Int

  init() {
    _a = ... // How can SwiftUI know this is _a
    _b = ... // How can SwiftUI know this is _b
  }
}

Remember that this can be called multiple times, one for each view update. Somehow SwiftUI can still know that State(...) refers to _a.

I'm most certain (from my own experiment, I couldn't find the doc anywhere) that you're not suppose to mutate during View init since that'd be in the body call of the parent view.

In any case, I believe the idiomatic way would be to use View.onAppear(perform:)

fileprivate struct EditingNumber: View {
    ....
    @State var draft: Int = 0
    
    var body: some View {
        VStack {
            ...
        }
        .navigationBarTitle(Text("Edit"))
        .onAppear {
            self.draft = self.score
        }
    }
}
1 Like

:thinking: you are right...interesting no runtime error like if you mutate any state inside var body ....

:grinning:I'm just glad there are people like you that can figure things out with zero documentation from Apple.

I still don't understand what you mean by:

The way I understand @State property wrapper thing is just like C macro, the compiler expand the var to a whole bunch of thing. Much to learn..

You're correct, more or less, but here's the thing, when SwiftUI calls body twice:

var body: some View {
  ...
  FooView()
}

You call FooView.init twice, which means you have two FooViews, but somehow SwiftUI is able to figure out that FooView.states from different FooViews refer to the same State, maintaining the illusion of persistence.

Since it's very common to use the compiler synthesized init to initialize view's @​State, very likely mutate state in init is allowed.

I think SwiftUI do "init phase" to generate the view body data structure, separate from "diff'ing/run/update" on this data structure.If so, then it's okay to mutate inside init's.

Wish Apple can just tell us a little...

Note that, in the synthesized init, it looks like this:

init() {
  _state = State(wrappedValue: ...)
}

as per the property wrapper proposal. While the warning refers to the mutation of the wrapped value like this:

// These two lines are equivalent
_state.wrappedValue = ...
state = ...

I'm a little surprise, as you've noted, that it doesn't set warning on

init() {
  state = ...
}

which would be warned inside body. But the result is very much unexpected still.

If @​State mutate in view init is not allowed, I think SwiftUI would surely give out warning like it does inside body. But this is just my guess.

The issue is that this validity is a framework (SwiftUI) issue, not the language, so not sure that can be even checked by the compiler

Not the compiler, SwiftUI does the runtime check: it give out runtime warning if you modify state inside body.

1 Like

I skipped most of the conversation. If you want to be on the safe side, don't try overriding State's initial value during init. It will only work once during very first view creation (important: not value initialization) and only if that view in the view tree gets re-created / replaced with a different view of the same type but internally different id.

If you really need that functionally here is what you could copy paste into your codebase: swiftui_helper_property_wrapper.swift · GitHub

This custom PW has the same API surface as State + it allows exactly what you want & you can even connect reactive streams into it. :exploding_head:

2 Likes

Oh and as a side note there is this artificial issue: [SR-12202] Property wrappers initialization is somehow artificially restricted for `State` · Issue #54627 · apple/swift · GitHub

Basically you cannot rely on the property = value syntax in the init if the PW has no mutable setter on its wrappedValue property. I understand why this is how it is right now, but theoretically the compiler should be able to diagnose this correctly and allow default initialization right through the wrapped property as a shorthand syntax (& kinda like late / out of line initialization) from within an init. That's why I consider this as a bug.

Related discussion: Why does this property wrapper code compile?

1 Like