Reputation: 443
I am learning SwiftUI and modified an existing example because I wanted to test if it is possible to have an ObservableObject accessible across views. When reading the documentation I found I should use the @EnvironmentObject for this. I tried this but this does not work: the array persons is filled with new persons (using the debugger) but the UI is not updated. If I use the people property outside of the @EnvironmentObject it works as expected.
My code:
import SwiftUI
struct ContentView: View {
@EnvironmentObject var envTest: EnvTest
var body: some View {
VStack {
ForEach(envTest.people.persons) { person in
Text("\(person.name)")
}
Button(action: {
self.envTest.people.persons.append(Person(id: 4, name: "I am new here"))
}) {
Text("Add/Change name")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static let envTest = EnvTest()
static var previews: some View {
ContentView().environmentObject(envTest)
}
}
class Person: ObservableObject, Identifiable {
var id: Int
@Published var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
class People: ObservableObject {
@Published var persons: [Person]
init() {
self.persons = [
Person(id: 1, name: "Jabba"),
Person(id: 2, name: "Polke"),
Person(id: 3, name: "Lori")]
}
}
class EnvTest: ObservableObject {
@ObservedObject var people = People()
}
I added the EnvTest to the SceneDelegate using:
var window: UIWindow?
var envTest = EnvTest()
func scene(...) {
let contentView = ContentView().environment(\.managedObjectContext, context).environmentObject(envTest)
}
Upvotes: 3
Views: 2788
Reputation: 41
There are two problems to fix to get to the result you want:
Combine
and replace @ObservedObject
in EnvTest
with @Published
EnvTest.objectWillChange
when the 'people' property changes by adding a subscriber to objectWillChange
of the people
property.The reason for these two changes is to do with how ObservableObject
works. The documentation tells us
An ObservableObject synthesizes an
objectWillChange
publisher that emits the changed value before any of its @Published properties changes.
If you don't have a @Published
property then the People.objectWillChange
publisher won't emit anything. Once we've fixed that, when we change the persons
property on People
that triggers the People.objectWillChange
publisher to emit a value. However, a second problem is revealed, that SwiftUI is listening to the EnvTest.objectWillChange
publisher, so we won't see the UI update. This is why we have to manually call send
on the EnvTest.objectWillChange
publisher when we receive a value from the People.objectWillChange
publisher, to inform SwiftUI that it needs to generate new Views.
It would be nice if Combine automatically propagated objectWillChange
calls when you put a @Published
property wrapper on a property of a type conforming to ObservableObject
. Similarly, it would be nice if SwiftUI watched the lowest ObservableObject
rather than the top level one (i.e. in this case watched People
instead of EnvTest
). Perhaps this will be introduced in the future, because the current solution of manually linking the two is a pain and won't scale well.
The full solution in code to do what you wanted is:
import SwiftUI
import Combine
struct ContentView: View {
@EnvironmentObject var envTest: EnvTest
var body: some View {
VStack {
ForEach(envTest.people.persons) { person in
Text("\(person.name)")
}
Button(action: {
self.envTest.people.persons.append(Person(id: 4, name: "I am new here"))
}) {
Text("Add/Change name")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static let envTest = EnvTest()
static var previews: some View {
ContentView().environmentObject(envTest)
}
}
class Person: ObservableObject, Identifiable {
var id: Int
@Published var name: String
init(id: Int, name: String) {
self.id = id
self.name = name
}
}
class People: ObservableObject {
@Published var persons: [Person]
init() {
self.persons = [
Person(id: 1, name: "Jabba"),
Person(id: 2, name: "Polke"),
Person(id: 3, name: "Lori")]
}
}
class EnvTest: ObservableObject {
@Published var people = People()
init(people: People = People()) {
self.people = people
people.objectWillChange.receive(subscriber: Subscribers.Sink(receiveCompletion: { _ in }) {
self.objectWillChange.send()
})
}
}
Upvotes: 4
Reputation: 443
Well, it seems this is not possible, but as my understanding of Swift got better, I know I can easily share the observable between views without embedding it in an EnvironmentObject.
Upvotes: 0