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

Continue from: Why SwiftUI @State property can be initialized inside `init` this OTHER way: - #23 by pyrtsa

So he found this difference:

struct ContentView: View {
    @State var value: Int?
    init() {
        value = 5  //has no effect
    }
    var body: some View {
        Text("\(value ?? -1)")  //shows -1 (nil) instead of 5
    }
}

But if the type is written as:

@State var value: Optional<Int>

Works

Why?

@hborla

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

Still another question here

struct ContentView: View {
    @State var a: Int?
    
    init() {
        a = 5
    }
    
    var body: some View {
        VStack {
            Text("\(a ?? -1)") // Show -1
        }
    }
}
struct ContentView: View {
    @State var a: Int?
    @State var b: Optional<Int>
    
    init() {
        a = 5
        b = 5
    }
    
    var body: some View {
        VStack {
            Text("\(a ?? -1)") // Show 5 (unexpected, why?)
            Text("\(b ?? -1)") // Show 5 (expected)
        }
    }
}

If we use debugger and disassemble it, we'll find that variable _a got init twice.

1.  `@State var a: Int?` // _a = State<Int?>(wrappedValue: nil)
2.  `a = 5` in init funcation // _a = State<Int?>(wrappedValue: 5) // Unexpected
1 Like

Oh wow! Double assignments and a third different outcome with the same code of optional variables . So you have both a and b using both optional spellings.

If you comment out b, now a is nil instead of getting value assigned in init!

struct FooFooFoo: View {
    @State var a: Int?
    @State var b: Optional<Int>     // 1. try comment this out
    
    
    init() {
        a = 1234
        b = 4444                    // 2. comment this out
    }
    
    var body: some View {
        VStack {
            Text("a = \(a ?? -1)")
            Text("b = \(b ?? -1)")  // 3. comment this out
        }
    }
}

So the exact same code behave differently depend on whether you have another optional variable is present or not!

Edit: see the conclusion to as why this is so: SwiftUI @State PW exact same code different result if another optional is added!? :=( - #9 by young

It's not an issue with State it looks a compiler issue.

struct S {
  var a: Int?
  var b: Optional<Int>

  init() {
    // error: Return from initializer without initializing all stored properties
  }
}

a will automatically be initialized with nil as a convenience feature, but b will not!

Related topics:

@Slava_Pestov @jrose maybe we should aim to remove this for Swift 6? The example of confusion about the effects it causes for State is a fair motivation. On top of that explicit Optional isn't treaded the same by the compiler anyways.

Edit: :man_facepalming: I meant to reply in this thread SwiftUI @State PW exact same code different result if another optional is added!? :=( but accidentally replied here and just realized that @mayoff already explained the puzzle.

1 Like

This must be hand crafted in SwiftUI because I have the opposite problem with the generated Decodable.init(from:): Property wrapper initialisation issues.

In the following example it's impossible (but I really hope I'm wrong) to access the 10 due to the way the compiler generates the initializer. Wrapper(importantValue: 10) is called before Cow(from:) but it's then entirely thrown away.

struct Cow: Codable {
    @Wrapper(importantValue: 10) var legs: UInt
    
    // This is generated by the compiler
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self._legs = try container.decode(Wrapper<UInt>.self, forKey: .legs)
    }
}

So this was once a bug that was closed: [SR-11777] 'var something: 'Type?' always get double initialised · Issue #54184 · apple/swift · GitHub