1

If I create an ObservableObject with a @Published property and inject it into a SwifUI view with .environmentObject(), the view responds to changes in the ObservableObject as expected.

class CounterStore: ObservableObject {
    @Published private(set) var counter = 0
    func increment() {
        counter += 1
    }
}

struct ContentView: View {
    @EnvironmentObject var store: CounterStore

    var body: some View {
        VStack {
            Text("Count: \(store.counter)")
            Button(action: { store.increment() }) {
                Text("Increment")
            }
        }
    }
}

Tapping on "Increment" will increase the count.

However, if I don't use the EnvironmentObject and instead pass the store instance into the view, the compiler does not complain, the store method increment() is called when the button is tapped, but the count in the View does not update.

struct ContentViewWithStoreAsParameter: View {
    var store: CounterStore

    var body: some View {
        VStack {
            Text("Count: \(store.counter) (DOES NOT UPDATE)")
            Button(action: { store.increment() }) {
                Text("Increment")
            }
        }
    }
}

Here's how I'm calling both Views:

@main
struct testApp: App {
    var store = CounterStore()
    
    var body: some Scene {
        WindowGroup {
            VStack {
                ContentView().environmentObject(store) // works
                ContentViewWithStoreAsParameter(store: store) // broken
            }
        }
    }
}

Is there a way to pass an ObservableObject into a View as a parameter? (Or what magic is .environmentalObject() doing behind the scenes?)

Jason Moore
  • 7,169
  • 1
  • 44
  • 45

2 Answers2

1

It should be observed somehow, so next works

struct ContentViewWithStoreAsParameter: View {
    @ObservedObject var store: CounterStore
//...
Asperi
  • 228,894
  • 20
  • 464
  • 690
1

You can pass down your store easily as @StateObject:

@main
struct testApp: App {
    @StateObject var store = CounterStore()
    
    var body: some Scene {
        WindowGroup {
            VStack {
                ContentView().environmentObject(store) // works
                ContentViewWithStoreAsParameter(store: store) // also works
            }
        }
    }
}

struct ContentViewWithStoreAsParameter: View {
    @StateObject var store: CounterStore

    var body: some View {
        VStack {
            Text("Count: \(store.counter)") // now it does update
            Button(action: { store.increment() }) {
                Text("Increment")
            }
        }
    }
}

However, the store should normally only be available for the views that need it, why this solution would make the most sense in this context:

struct ContentView: View {
    @StateObject var store = CounterStore()

    var body: some View {
        VStack {
            Text("Count: \(store.counter)")
            Button(action: { store.increment() }) {
                Text("Increment")
            }
        }
    }
}
lr058
  • 273
  • 2
  • 11
  • I prefer to not create models (stores) in my views, but to create them once at a higher level and then inject them as needed. From reading more about the differences of ObservedObject and StateObject, it seems that for passing objects into views, it is better to use ObservedObject. StateObject is useful if you create your store in your view since it is not destroyed when the view is redrawn. – Jason Moore May 11 '22 at 14:21
  • While i do not think it makes sense to create your models at a levels where they are not needed, if you do it is better to use `@ObservedObject` (however, if you would share your model with multiple views your approach would not work anymore). Further explanation: https://stackoverflow.com/a/62544554/9022641 – lr058 May 12 '22 at 07:53
  • As I said, I don't create ObservedObjects (the data model) in views, but instead create them at a higher level and inject them as needed. Multiple views are able to be updated as the ObservedObject changes. – Jason Moore May 17 '22 at 15:07