This is due to an artificial limitation in the compiler's implementation of property wrappers. The workaround is to initialize the backing storage directly via _value in V.init:
The limitation is due to nonmutating set of State‘s wrappedValue. This isn‘t related to only State but a general property wrapper limitation created by its current implementation. You can create an own test PW and test it and see the compiler complaining just like with State. And yes I‘ve seen the comment as I created the bug ticket and got all notifications. ;)
The value in State will always be initialized with the value you pass in the init, this is simple Swift. However before next body call SwiftUI will call update method and reinject a value into it if there was a previous value which will override your value from init. The whole logic is as simple as that.
You can observe this by using Mirror from init and body.
I spend hours and hours digging in these small details, you either trust or nor in what I just tried to explain you.
The synthesized function is different from what we wrote:
// Synthesized init would look like this
init() {
_bar = State(wrappedValue: 123)
}
// What we wrote
init() {
bar = 123
}
// Which instead becomes (IIUC from that bug report)
init() {
_bar = State() // error
bar = 123
}
IMO, this sounds as close to undefined as it can get.
@hborla say it's a compiler limitation that we cannot write:
init() {
bar = 123
}
that we should do this workaround:
init() {
_bar = State(wrappedValue: 123)
}
I tried @DevAndArtist's sample. I don't know what it shows so I can't tell if the problem is real or not.
If overriding @State var is bad, then how can this kind of common place code work then?
import SwiftUI
struct Engine: View {
@State var cylinder = 8 // can be overriden on init, so problem or not?
var body: some View {
Stepper(value: $cylinder) {
Text("Engine cylinder: \(cylinder)")
}
}
}
struct InitAtStateAllowOrNot: View {
var body: some View {
VStack {
Engine(cylinder: 100) // override cylinder var, not okay?
Engine(cylinder: 44)
}
}
}
struct InitAtStateAllowOrNot_Previews: PreviewProvider {
static var previews: some View {
InitAtStateAllowOrNot()
}
}
If you're curious about how the synthesiser interacts with the property wrapper, you can make your own mock-up.
@propertyWrapper
struct Foo {
var wrappedValue: Int {
get {
print("Getting")
return 0
}
// Non-mutating for extra effect
nonmutating set {
print("Setting")
}
}
init(wrappedValue: Int) {
print("Initting")
}
}
and go wild.
To be fair, most of the time you shouldn't need to be concerned about this kind of things as they are indistinguishable, whether to init with the correct value, or change it afterward. It's just that the implementation of State makes this kind of difference visible.
There is no magic here. This code above is translated to this:
struct Foo: View {
private var _bar: State<Int>
var bar: Int {
get {
_bar.wrappedValue
}
nonmutating set {
_bar.wrappedValue = newValue
}
}
init() {
self._bar = State(wrappedValue: 123)
}
}
I highly encourage you to re-read the PW proposal from top to bottom.
Also my example shows what State contains after init and during body call to demonstrate that its own internal value gets swapped by the framework.
State behaves exactly like normal Swift struct, but its a framework guarantee that it will have a different value reinject when you read from within body iff there was a previous value that the framework cached.
It's just that the rules does sound very convoluted (kudos to you for figuring that out!). Makes me think we're entering the area that's not intended to be used.
The only rule for State in particular is that you should not initialize its value from the parent. If you do, then you need to know what will happen. The initialization part is really pure Swift as we know it. The re-injection is the framework thing. I don't think it's near undefined, but it's definitely not intuitive enough for many developers out there.
I can see the problem now, this simple example shows it:
import SwiftUI
struct ChildViewEngine: View {
// every subsequent construct of this view, the @State gets a new value
// but the view shows only the very first time value
// it's as if 'body' somehow is stuck at referencing the very first value
@State var cylinder: Int
// why it doesn't work like a plain variable?
let duh: Int
var body: some View {
VStack {
Text("@State var cylinder does not change :(")
Text("Engine - cylinder: \(cylinder), duh: \(duh)")
.background(Color.yellow)
.padding()
.background(Color.orange)
}
}
}
struct ParentViewInitAtStateAllowOrNot: View {
@State var number = 100
var body: some View {
NavigationView {
VStack {
// ChildViewEngine child view should get a new cylinder value but it's not
ChildViewEngine(cylinder: number, duh: number)
// the very same ChidViewEngine going through NavigationLink do not have this problem
NavigationLink(destination: ChildViewEngine(cylinder: number, duh: number)) {
Text("Go to Engine View")
}
Stepper(value: $number) {
Text("Engine cylinder: \(number)")
}
}
}
}
}
struct InitAtStateAllowOrNot_Previews: PreviewProvider {
static var previews: some View {
ParentViewInitAtStateAllowOrNot()
}
}
It seem any @State var inside child view, any subsequent construct of new one, its body still refers to the very first time init copy of @State var. NavigationLink do not have this problem.
This behavior is very unexpected. Must be a bug? I don't remember see any rule in SwiftUI that disallow this kind of code.
This is like "a View @State var body dismemberment syndrome"
This is NOT a bug! It works as expected/advertised. What‘s the point of adding State to a view property if it would work like a plain constant? State property wrapper adds one possible way of mutation to the view, which also is meant to be private to the view, not initialized from parent, etc. Every view in SwiftUI is completely immutable.
Long story short, it‘s a little bit tiring to reiterate the whole thing over and over.
view gets created from some parent view
view has a property wrapped with State
SwiftUI caches that value
same view or subview causes the value to change that is referenced by that State
SwiftUI updates the cached value to the new value
only that view and its view subtree will receive a call to body
if a parent’s view value mutates, our current view gets recreated (the struct value describing our view, not the rendered view - that‘s a different story as a type conforming to View is not really a view, nor should the protocol be named like that, but that ship has sailed)
you don‘t expect our current view to get its value reset as it would be equivalent with data loss
before body call of our view, SwIftUI restores the value in State to the cached value it remembers last (EXPECTED BEHAVIOR)
I'm not sure about the written documentation but the expectation that @State should be internal to a view was reiterated several times in the WWDC presentations introducing SwiftUI.
Exactly this. Perhaps undefined is the wrong word since you more-or-less defined it in two lines. But it feels like a side-effect of the underlying mechanism and an interplay between so many pieces, rather than a conscious part of the framework design. The nuance of which is irrelevant to this post. And we do agree that this is not something you'd want to do on regular basis:
So is this just some caching problem with no way of invalidating the cache? They must've opt'ed for some good reason to cache, causing it to only allow one/very first init'able only.
We can simply replace @State with @ObservedObject (or your Model PW) to overcome this problem. So it's not a problem at all once you'er aware of the problem. Am I missing something, is this kind of use case not good?
The API do not prevent/warn about improper @State usage. Seems like a bad thing. Wish there is some warning like @State mutate in body runtime warning. But how can they warn because it's fine if it's used in a NavigationLink...
It's not enforceable by the compiler, therefore there is no explicit warning or error. SwiftUI already traps if you try to use some property wrappers outside body which tells you the framework contract.
To conclude the discussion, there is NO problem here except people misunderstanding what State is build for. I mentioned it above already and you should really think about this. Every so called "View" in SwiftUI is completely immutable. Even if you add var foo: Type, you won't be able to mutate it because body is read only, because it's a non mutating property. To allow mutation and local / referenced storage or models, SwiftUI provides you things like State (local), ObservedObject (referenced), Environment (local copy), EnvironmentalObject (referenced), etc. It's been mentioned upthread dozen times, it's been mentioned during WWDC and on a lot of blog posts that State should mimic local / view private storage. State is a property wrapper that tells the framework "hey for this view I need some mutable storage, can you please create it for me?!". The framework then creates a referenced storage (hence nonmutating set on State's wrappedValue) for this unique view and injects it into your State before each body call. This is transparent to the user as it's an implementation detail and simply the way this property wrapper and mutation is designed for SwiftUI. This storage is initialized with the very first value you provide, which could be either through the default property value or through the init. However if you do it through the init, it won't reach the referenced storage the framework creates anymore, except in the case where the same view is replaced by another similar view but which internally has a different unique id. In that case the old storage is destroyed and you get a new one. If you happen to update the wrapped property, SwiftUI will update the referenced storage with the new value, hence I said it's cached. Long story short, there is no caching problem, there is no real problem with State, it works as advertised. The only issue is that people have hard times understanding these things.