My goal is to have multiple views with a two-way binding to the same variable. I have created a view with two embedded views that all update the same ObservableObject. However, when one view updates it, only that view can see the change. None of the others get updated. The code compiles just fine, but the views act as if they each have a local variable instead of sharing a published one.
What have I missed in how to make this work?
import SwiftUI
class UserSettings: ObservableObject {
@Published var score:Int = 0
}
struct ButtonOne: View {
@ObservedObject var settings = UserSettings()
var body: some View {
HStack {
Button(action: {
self.settings.score += 1
}) {
Text("Increase Score")
}
Text("In ButtonOne your score is \(settings.score)")
}
}
}
struct ButtonTwo: View {
@ObservedObject var settings = UserSettings()
var body: some View {
HStack {
Button(action: {
self.settings.score -= 1
}) {
Text("Decrease Score")
}
Text("In ButtonTwo your score is \(settings.score)")
}
}
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack(spacing: 10) {
Text("In master view your score is \(settings.score)")
ButtonOne()
ButtonTwo()
Text("All scores refer to the same variable, so should be the same.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
The views do indeed each have a local object, because as @DreamWorld Development pointed out you're assigning each to the result of an initializer, creating separate instances:
@ObservedObject var settings = UserSettings()
Each view thus owns its own UserSettings instance.
In SwiftUI, your aim should be to have something owned in one place, then referenced in others. @ObservedObject is one way to do this, though you need to explicitly allocate it once and pass it to your child views. Alternatively you can use the $-syntax to pass a Binding to one of its published variables into a child view. Alternatively you can make a single ObservableObject available to an entire view tree by assigning it to the environment.
In the latter case, you'd do something like this:
class UserSettings: ObservableObject {
@Published var score:Int = 0
}
struct ButtonOne: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
HStack {
Button("Increase Store") {
self.settings.score += 1
}
Text("In ButtonOne your score is \(settings.score)")
}
}
}
struct ButtonTwo: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
HStack {
Button("Decrease Score") {
self.settings.score -= 1
}
Text("In ButtonTwo your score is \(settings.score)")
}
}
}
struct ContentView: View {
// Fetched from the environment on your behalf
@EnvironmentObject var settings: UserSettings
var body: some View {
VStack(spacing: 10) {
Text("In master view your score is \(settings.score)")
// Buttons inherit the environment, including the UserSettings instance.
ButtonOne()
ButtonTwo()
Text("All scores refer to the same variable, so should be the same.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environmentObject(UserSettings()) // assign environment
}
}
Here the single UserSettings() instance is created by the preview provider (in the real app, this would happen in your SceneDelegate) and placed in the ContentView's environment using the .environmentObject() modifier. The ContentView and both buttons then access it by declaring a property of the appropriate type with the @EnvironmentObject attribute. SwiftUI will fetch it from the environment on demand.
Now, if this is indeed a general object containing globally-useful things, then placing it within the environment is the right way to do things. If it only contains information useful within a general hierarchy, you're better off using bindings. The type of setup you have in your example lends itself to that. Here you'd declare and own your UserSettings within the ContentView, and would pass a binding to the score property into each button:
class UserSettings: ObservableObject {
@Published var score:Int = 0
}
struct ButtonOne: View {
// Initialize the view with a binding to use.
@Binding var score: Int
var body: some View {
HStack {
Button("Increase Store") {
// modify the binding directly.
self.score += 1
}
Text("In ButtonOne your score is \(score)")
}
}
}
struct ButtonTwo: View {
// Initialize the view with a binding to use.
@Binding var score: Int
var body: some View {
HStack {
Button("Decrease Score") {
// modify the binding directly.
self.score -= 1
}
Text("In ButtonTwo your score is \(score)")
}
}
}
struct ContentView: View {
// ContentView owns this object.
@ObservedObject var settings = UserSettings()
var body: some View {
VStack(spacing: 10) {
Text("In master view your score is \(settings.score)")
// Buttons inherit the environment, including the UserSettings instance.
ButtonOne(score: $settings.score)
ButtonTwo(score: $settings.score)
Text("All scores refer to the same variable, so should be the same.")
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView() // no special treatment here.
}
}
This way you're being more explicit about what state is shared, and with whom. The buttons only use the score property, so that's all they're given.
In fact, as it currently stands, the binding approach doesn't even require @ObservedObject or @Published—you can simply make UserSettings a structure type and use the @State attribute in your ContentView. This has the added effect of simplifying the memory model within your application, which is generally a Good Thing.
So, for example, just change the following parts of the above sample:
// This is a value type now, rather than a class.
struct UserSettings {
var score:Int = 0
}
struct ButtonOne: View {
// no changes
}
struct ButtonTwo: View {
// no changes
}
struct ContentView: View {
// ContentView owns the state data.
@State var settings = UserSettings()
var body: some View {
// no changes
}
}
struct ContentView_Previews: PreviewProvider {
// no changes
}