@State of optional: type spelling Optional<Int> is not the same as Int?

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).

11 Likes