Updating ObservableObject from multiple views

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()
    }
}
Add a Comment

Accepted Reply

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
}
  • [@Jim Dovey](https://developer.apple.com/forums/profile/Jim Dovey) Why did we create the @environment in root class? shouldnt it be @stateObject or @ObservedObject and then passto .enviroment object?

  • struct ContentView: View { @ObservedObject var settings: UserSettings //OR // @StateObject 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.") }.environmentObject(settings) } }

Add a Comment

Replies

you call the initializer also on the other views. omit them and pass it over from one to the other view.

The error was to create instances of UserSettings in Buttons/

Just have to declare a var type.



This works:


import SwiftUI

class UserSettings: ObservableObject {
    @Published var score:Int = 0
   
}
 
struct ButtonOne: View {
//    @ObservedObject var settings = UserSettings()
    @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(settings: settings)
            ButtonTwo(settings: settings)
            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
}
  • [@Jim Dovey](https://developer.apple.com/forums/profile/Jim Dovey) Why did we create the @environment in root class? shouldnt it be @stateObject or @ObservedObject and then passto .enviroment object?

  • struct ContentView: View { @ObservedObject var settings: UserSettings //OR // @StateObject 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.") }.environmentObject(settings) } }

Add a Comment

To use environmentObject, declare and set in SceneDelegate as told by Jim:


class SceneDelegate: UIResponder, UIWindowSceneDelegate { 
 
    var window: UIWindow? 
    var settings = UserSettings()   // For environment object 
 
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 
        // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 
        // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 
        // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 
 
        // Create the SwiftUI view that provides the window contents. 
        let contentView = ContentView() 
 
        // Use a UIHostingController as window root view controller. 
        if let windowScene = scene as? UIWindowScene { 
            let window = UIWindow(windowScene: windowScene) 
            window.rootViewController = UIHostingController(rootView: contentView.environmentObject(settings)) 
            self.window = window 
            window.makeKeyAndVisible() 
        } 
    } 
 
// Usual next for SceneDelegate

This is seriously the best answer I've gotten in a long time in a Forum. Thank you SO much. It not only explained and illustrated how to solve my issue but clarified the underlying principles of solving it, allowing me to solve other issues down the line.

The other answers are great too, but this one will go into my quickly-evolving-soon-to-be-copious notes on SwiftUI and be referred back to until it's second nature.

Thank you very much. This bit here really nailed for me what I was doing wrong:

//    @ObservedObject var settings = UserSettings()  
    @ObservedObject var settings : UserSettings 


The reason I chose Jim's answer as the correct one, is that he included information on how to achieve the same thing in different ways.

This is very good answer