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