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.
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
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
...
}
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?
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.
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.
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.
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.