I'm surprised SwiftUI doesn't issue a diagnostic when it runs your Int?
code.
Swift treats Int?
and Optional<Int>
differently in the following way:
-
A var
property of type Int?
with no initial value is initialized to nil
before init
is entered, as though you had written = nil
in the property declaration.
-
A var
property of type Optional<Int>
with no initial value is not initialized before init
is entered.
Example:
struct Test {
var a: Int? // Swift pretends you wrote ... = nil
var b: Optional<Int>
init() { }
// error: Return from initializer without initializing all stored properties
// info: 'self.b' not initialized
}
So Swift treats your @State var value: Int?
like this:
@State var value: Int? = nil
and through the magic of property wrappers, Swift effectively puts this at the start of init
:
_value = State<Int?>.init(wrappedValue: nil)
Then, when it reaches the line value = 5
in init
, it translates it to this:
_value.wrappedValue = .some(5)
To understand why that has no effect, we need to understand how State
stores its value.
State
has two stored properties:
@frozen @propertyWrapper public struct State<Value> : SwiftUI.DynamicProperty {
@usableFromInline
internal var _value: Value
@usableFromInline
internal var _location: SwiftUI.AnyLocation<Value>?
etc. etc.
}
You might think it stores its live value in that _value
property, but in fact it only stores its initial value there. It stores its “live” value indirectly through _location
, which points to storage managed by SwiftUI.
The problem is that SwiftUI can't allocate that storage until it gets its hands on your ContentView
, which can't happen until ContentView.init
returns. So at the point of value = 5
, _location
is nil
.
Now, you might think that the wrappedValue
setter would see that _location
is nil
and update _value
instead, but it doesn't. It discards the new value in that case. If you disassemble the setter, you'll find that it is roughly this:
var wrappedValue: Value {
nonmutating set {
guard let _location = _location else {
return
}
_location.set(newValue)
}
}
I'm honestly surprised that SwiftUI doesn't update _value
in this case, and also doesn't emit a diagnostic. If, for example, you print($value)
in init
, it issues this diagnostic:
Accessing State's value outside of being installed on a View. This will result in a constant Binding of the initial value and will not update.
Anyway, you can work around it by setting _value
directly:
struct ContentView: View {
@State var value: Int?
init() {
_value = .init(wrappedValue: 5)
}
var body: some View {
Text("\(value ?? -1)") // shows 5 now
}
}
In the case where you wrote @State var value: Optional<Int>
, Swift doesn't automatically insert an initialization of _value
at the start of init
. So when Swift sees the value = 5
line, it knows that this line must be the initialization of _value
, and translates it to _value = .init(wrappedValue: nil)
.