Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to update/modify SwiftUI View's @state var

I'm unable to update the ExampleView's message var even though I can see updateMessage() is being called. Here is my simplified/convoluted SwiftUI example of Playground code that isn't working. The message var does not get updated when called in updateMessage(). As a result, the UI Text() is not updated either.

Why is the @State var message not updating? What is the correct way to update it?

import SwiftUI
import PlaygroundSupport

struct ContentView: View {
    let coloredLabel = ExampleView()

    var body: some View {
        VStack {
            coloredLabel
                .foregroundColor(Color.red)
                .padding()
            Button(action: {
                self.coloredLabel.updateMessage()
            }) {
                Text("Press me")
            }

        }
    }
}

struct ExampleView: View {
    @State private var message: String = "Hello"

    var body: some View {
        Text(self.message)
    }

    func updateMessage() {
        print("updateMessage ran") // this prints
        self.message = "Updated"
    }
}

PlaygroundPage.current.setLiveView(ContentView())
like image 556
xta Avatar asked Jan 17 '20 08:01

xta


People also ask

How do I change my state variable in SwiftUI?

In case we have to modify state when another state is known, we can encapsulate all those states in ObservableObject and use onReceive to check the state we want to act on. Modifying state during view update, this will cause undefined behavior.

What is state var SwiftUI?

With @State, you tell SwiftUI that a view is now dependent on some state. If the state changes, so should the User Interface. It's a core principle of SwiftUI: data drives the UI.

How do I refresh a view in SwiftUI?

To add the pull to refresh functionality to our SwiftUI List, simply use the . refreshable modifier. List(emojiSet, id: \. self) { emoji in Text(emoji) } .


2 Answers

You should only change State of a view inside its 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)
    }
}
like image 67
Mojtaba Hosseini Avatar answered Sep 26 '22 14:09

Mojtaba Hosseini


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 and ExampleView: if state.message (any value that state 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.

like image 26
Alex Avatar answered Sep 26 '22 14:09

Alex