SwiftUI @State PW exact same code different result if another optional is added!? :=(

This code both a and b get the initialize value in the 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
        }
    }
}

and the view show:

a = 1,234
b = 4,444

But if you just simply comment out all b:

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

you now get a different result:

a = nil

there is additionally this double initialization problem.

3 Likes

Or instead of commenting out just change the order of assignments:

    a = 1234
    b = 4444

outputs:
a = 1234, b = 4444

    b = 4444
    a = 1234

outputs:
a = nil, b = 4444

Note, it works ok with a more elaborate assignment:

_a = .init(wrappedValue: 1234)

Is this not a big problem that code behave differently depending on existence of or order of independent variables?

Always use this syntax:

@State var b: Optional<Int>

and not:

@State var b: Int?

This is only a problem with @State and not Swift the language.

Worth filing a bug against SwiftUI.

Isn't the problem here caused by Int? having a default value, while Optional<Int> doesn't? @State just runs into the issue harder than normal due the fact that SwiftUI caches the initial value. Seems like a language issue to me. Hopefully one we can fix in Swift 6 (by removing the implicit = nil from T? variables).

3 Likes

Removing implicit initialization of optionals has already been discussed several times. There’s a more fundamental issue here, which is that initializer expressions aren’t just a convenient shorthand for assignment statements at the beginning of the initializer.

2 Likes

Interesting, could you point to some further reading about this?

My information might be out-of-date. It was definitely once the case that initialization expressions were emitted as non-inlineable functions, and the consequences of are most apparent when switching between implicitly-initialized Foo? and explicitly-initialized Optional<Foo>. That might have changed with SR-11768, but my reading of the discussion is that it only changed for @frozen types.

2 Likes

With SwiftUI @State (not sure if its this apply to other PW), the auto generated nil initialization is conditional: it depends on whether there are other PW and if they are initialized. If not, no implicit initialization to nil!

So using my example:

@State private var a: Int?  // Will this be already nil by init? Maybe
@Stste private var b: Optional<Int>  // if this is initialized here then a above is auto nil, if not, no auto nil!

This basically sums up the whole problem that prompted me to start this thread and that other original thread.

Isn't the problem here caused by Int? having a default value

Look out! This doesn't always get auto initialized to nil. This is what's fascinating about this.

How would you describe this bug? Is this a SwiftUI or Swift bug?

My minimum sample code demonstrating the problems:

import SwiftUI


// this is the baseline version and see how thing can change by simply re-arraging init order or initialization of `b`
struct ContentViewCase1: View {
    @State var a: Int?
    @State var b: Optional<Int> // ! b/c this is not initialized,
    
    
    init() {
        a = 1234
        b = 4444
    }
    
    var body: some View {
        VStack {
            Text("a = \(a.map({ $0.formatted() }) ?? "nil")")
            Text("b = \(b.map({ $0.formatted() }) ?? "nil")")
        }
    }
}




// the following two views should be the same as above but is not
// how do you describe this bug to apple for filing a feedback to SwiftUI
// or is this a Swift lang bug?

struct ContentViewCase2: View {
    @State var a: Int?
    @State var b: Optional<Int> // ! b/c this is not initialized,
    
    
    init() {
        b = 4444        // ! just reverse the order and we get another result
        a = 1234
    }
    
    var body: some View {
        VStack {
            Text("a = \(a.map({ $0.formatted() }) ?? "nil")")
            Text("b = \(b.map({ $0.formatted() }) ?? "nil")")
        }
    }
}


struct ContentViewCase3: View {
    @State var a: Int?
    @State var b: Optional<Int> = 9999      // by assigning something here
    
    
    init() {
        a = 1234        // a can no longer be initialized because due to b being init'ed, it get init'ed, unlike case 1
        b = 4444   
    }
    
    var body: some View {
        VStack {
            Text("a = \(a.map({ $0.formatted() }) ?? "nil")")
            Text("b = \(b.map({ $0.formatted() }) ?? "nil")")
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentViewCase1()
        ContentViewCase2()
        ContentViewCase3()
    }
}

Initially when learning I was in confused state too (pun intended :crazy_face:). This is just how property wrappers work. It helped to write a property wrapper and go through all the permutations of initializing to understand what overrides in each step of the process.

Choose an init style:

@State var a: Int? = 1234

or

@State(wrappedValue: 1234) var a: Int?

or

init() {
       self._a = State(wrappedValue: 1234)
}

Reference: https://www.swift.org/blog/property-wrappers/ has links to other great sources.

So you’re correct this is how properly wrapper are initialized. However, @State is unique in that its initial value is “cached” and is used to create the view no matter how you initialize the @State: any subsequent initialize value has no effect.

However, you can give the view a different id then you can re-initialize @State

import SwiftUI

struct ContentView: View {
    @State private var value = 1234.5667
    
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
            Slider(value: $value, in: 0...100, step: 1)
            ChildOne(Int(value))
            ChildOne(Int(value))
                .id(value)
        }
        .padding()
    }
}

struct ChildOne: View {
    @State private var a: Int
    let x: Int
    
    init(_ a: Int) {
        self.a = a
        x = a
    }
    
    var body: some View {
        Text("a = \(a), x = \(x)")
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

This is kind of unexpected and it’s something important to know about SwiftUI.