Unable to update/modify SwiftUI View's @state var
You should only change State
of a view Inside it's own body block. If you need to change it from a parent view, you may want to pass the value to it from parent and make it Binding
instead.
struct ContentView: View {
@State private var message = "Hello"
var body: some View {
VStack {
ExampleView(message: $message)
.foregroundColor(Color.red)
.padding()
Button("Press me") {
self.message = "Updated"
}
}
}
}
struct ExampleView: View {
@Binding var message: String
var body: some View {
Text(message)
}
}
If you need to encapsulate messages inside the ExampleView
, you can use a Bool
(or an enum
or etc) instead:
struct ContentView: View {
@State private var updated = false
var body: some View {
VStack {
ExampleView(isUpdated: $updated)
.foregroundColor(Color.red)
.padding()
Button("Press me") {
self.updated = true
}
}
}
}
struct ExampleView: View {
@Binding var isUpdated: Bool
private var message: String { isUpdated ? "Updated" : "Hello" }
var body: some View {
Text(message)
}
}
actually, the variable does get updated, but your Content view doesn't get informed about it. This is what happens:
- ContentView gets called, it initializes coloredLabel with an ExampleView
- you press the button in ContentView
- self.coloredLabel.updateMessage() get's called
- the message is printed
- the variable self.coloredLabel.message is modified
- ContentView does not get redrawn, as it isn't notified about the change
- more specifically, coloredLabel inside your Stack doesn't get updated
now, you have different options:
@State
, @Binding
and @PublishedObject, @ObservedObject
. You need one of these Publishers, so your view actually notices that it needs to do something.
Either you draw a new ExampleView
every time you press the button, in this case you can use a @State
variable in ContentView
:
struct ContentView: View {
@State private var string = "Hello"
var body: some View {
VStack {
ExampleView(message: string)
.foregroundColor(Color.red)
.padding()
Button(action: {
self.string = "Updated"
}) {
Text("Press me")
}
}
}
}
struct ExampleView: View {
var message: String
var body: some View {
Text(self.message)
}
}
which is probably not what you want.
Next, you can use @Binding which was already suggested.
And last, you can use ObservableObject @ObservedObject, @Published
class ExampleState: ObservableObject {
@Published var message: String = "Hello"
func update() {
message = "Updated"
}
}
struct ContentView: View {
@ObservedObject var state = ExampleState()
var body: some View {
VStack {
ExampleView(state: state)
.foregroundColor(Color.red)
.padding()
Button(action: {
self.state.update()
}) {
Text("Press me")
}
}
}
}
struct ExampleView: View {
@ObservedObject var state: ExampleState
var body: some View {
Text(state.message)
}
}
what this says is:
class ExampleState: ObservableObject
- this class has published variables that can be observed
to resume (that's how I understand it):
- "Hey,
ContentView
andExampleView
: ifstate.message
(any value thatstate
publishes) changes, you need to redraw your body" - "And
ExampleState
: after updating your message variable, publish the new value!"
lastly - for completion - there is @EnvironmentObject
, as well, that way you'd only have to pass the variable to the top-views and everything down the view hierarchy would inherit it.