@State's initial value doesn't change on re-render

Problem Statement

I'm having trouble understanding how to reset a @State property to its initial value.

In my app, I have one view that sets an integer "default value". The other view reads it, but doesn't change it:

Code Block
struct ContentView: View {
    @State var selectedValue = 1
    var body: some View {
        VStack{
            DefaultValueView(defaultValue: $selectedValue)
            ComputeView(defaultValue: selectedValue)
        }
    }
}


The first view is simple: it uses a @Binding to the selected value and updates it based on the user's selection. Here, we let the user choose a default value between 1 and 10:

Code Block
struct DefaultValueView: View {
    @Binding var defaultValue: Int
    var body: some View {
        VStack {
            Text("Select the default value")
            ForEach(1...10, id: \.self) { i in
                Button("Value: \(i)") {
                    defaultValue = i
                }.background(i == defaultValue ? Color.red : Color.clear)
            }
        }
    }
}


In a second, I want to have the following behavior:
  • When the selectedValue changes, it displays the selected value.

  • The user can increment or decrement the displayed value. This doesn't change the selectedValue, just what's displayed in this view.

My attempt was this:
Code Block
struct ComputeView: View {
    @State var additionalValue: Int
    
    init(defaultValue: Int) {
        _additionalValue = State(initialValue: defaultValue)
    }
    
    var body: some View {
        VStack {
            Text("10 plus..")
            Stepper("\(additionalValue)") {
                additionalValue += 1
            } onDecrement: {
                additionalValue -= 1
            }
            Text("= \(10 + additionalValue)")
        }.padding()
    }
}


This achieves the 2nd behavior (I can edit the displayed value with the stepper) but not the 1st. When I change "default value" to something else, it doesn't change in this view. I've got a breakpoint in the init method, so I know it's being re-set to the new initialValue, but it doesn't change.


What am I missing?







Full app to copy/paste:


Code Block
import SwiftUI
@main
struct DefaultEditableValueApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}
struct ContentView: View {
    @State var selectedValue = 1
    var body: some View {
        VStack{
            DefaultValueView(defaultValue: $selectedValue)
            ComputeView(defaultValue: selectedValue)
        }
    }
}
struct DefaultValueView: View {
    @Binding var defaultValue: Int
    var body: some View {
        VStack {
            Text("Select the default value")
            ForEach(1...10, id: \.self) { i in
                Button("Value: \(i)") {
                    defaultValue = i
                }.background(i == defaultValue ? Color.red : Color.clear)
            }
        }
    }
}
struct ComputeView: View {
    @State var additionalValue: Int
    
    init(defaultValue: Int) {
        _additionalValue = State(initialValue: defaultValue)
    }
    
    var body: some View {
        VStack {
            Text("10 plus..")
            Stepper("\(additionalValue)") {
                additionalValue += 1
            } onDecrement: {
                additionalValue -= 1
            }
            Text("= \(10 + additionalValue)")
        }.padding()
    }
}

Accepted Reply

What you described is the expected behavior of @State in SwiftUI.

The struct State initially does not have a storage for the state, SwiftUI allocates it when a View is established. (Not when it is initialized.)
The initializer of a SwiftUI View is called at any time SwiftUI needs it, and if the storage for the state is already allocated, SwiftUI uses the already allocated storage keeping the value. In such cases, initialValue is simply ignored.

Thus, init is not a good place to update the value of @State vars.

If you want to detect the selectedValue changes, you need to explicitly implement some mechanisms to detect the changes.
An example:
Code Block
import SwiftUI
@main
struct DefaultEditableValueApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class SelectedState: ObservableObject {
@Published var value: Int = 1
}
struct ContentView: View {
@StateObject var selectedState = SelectedState()
var body: some View {
VStack{
DefaultValueView(defaultValue: $selectedState.value)
ComputeView(selectedState: selectedState)
}
}
}
struct DefaultValueView: View {
@Binding var defaultValue: Int
var body: some View {
VStack {
Text("Select the default value")
ForEach(1...10, id: \.self) { i in
Button("Value: \(i)") {
defaultValue = i
}.background(i == defaultValue ? Color.red : Color.clear)
}
}
}
}
struct ComputeView: View {
@State var additionalValue: Int = 0
@ObservedObject var selectedState: SelectedState
var body: some View {
VStack {
Text("10 plus..")
Stepper("\(additionalValue)") {
additionalValue += 1
} onDecrement: {
additionalValue -= 1
}
Text("= \(10 + additionalValue)")
}.padding()
.onReceive(selectedState.$value) {value in
additionalValue = value
}
}
}


Replies

What you described is the expected behavior of @State in SwiftUI.

The struct State initially does not have a storage for the state, SwiftUI allocates it when a View is established. (Not when it is initialized.)
The initializer of a SwiftUI View is called at any time SwiftUI needs it, and if the storage for the state is already allocated, SwiftUI uses the already allocated storage keeping the value. In such cases, initialValue is simply ignored.

Thus, init is not a good place to update the value of @State vars.

If you want to detect the selectedValue changes, you need to explicitly implement some mechanisms to detect the changes.
An example:
Code Block
import SwiftUI
@main
struct DefaultEditableValueApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
class SelectedState: ObservableObject {
@Published var value: Int = 1
}
struct ContentView: View {
@StateObject var selectedState = SelectedState()
var body: some View {
VStack{
DefaultValueView(defaultValue: $selectedState.value)
ComputeView(selectedState: selectedState)
}
}
}
struct DefaultValueView: View {
@Binding var defaultValue: Int
var body: some View {
VStack {
Text("Select the default value")
ForEach(1...10, id: \.self) { i in
Button("Value: \(i)") {
defaultValue = i
}.background(i == defaultValue ? Color.red : Color.clear)
}
}
}
}
struct ComputeView: View {
@State var additionalValue: Int = 0
@ObservedObject var selectedState: SelectedState
var body: some View {
VStack {
Text("10 plus..")
Stepper("\(additionalValue)") {
additionalValue += 1
} onDecrement: {
additionalValue -= 1
}
Text("= \(10 + additionalValue)")
}.padding()
.onReceive(selectedState.$value) {value in
additionalValue = value
}
}
}


This is great, thank you!

Quick follow-up. I want to persist the selected value in scene storage. Before typing this up, I was doing that by replacingContentView's @State with @SceneStorage('selectedValue'). While that stored it, it still had the "updating the default does nothing" problem.

Is it safe to assume that, now that I'm in the realm of ObservableObject that I need to write my own state storage and restoration, rather than use something like @SceneStorage?

Is it safe to assume that, now that I'm in the realm of ObservableObject that I need to write my own state storage and restoration, rather than use something like @SceneStorage?

Frankly, I do not have enough experience with @SceneStorage. But as far as I have tried till now, it is very similar to @State and hard (or at least I do not know how) to detect changes of such variables.
Using @Published is one of the simplest ways of detecting changes, but with using it you cannot expect the automatic state restoration.

So, in my opinion, the answer is YES, it is safe assume that you need to write your own state storage and restoration.