Reputation: 58903
The following code is the minimal reproducible example extracted from a problem I'm facing in a much larger app. With the following code I'm trying to:
Container
) with an observable array of observable objectsObjectVisualizer
) with the passed data reflect the change done to the object by redrawing the array with the changed propertyThe problem is that any change to any object is not reflected. How to make it work?
class MyObservedObject: Hashable, Equatable, ObservableObject {
@Published var text: String
init(_ text: String) {
self.text = text
}
static func == (lhs: MyObservedObject, rhs: MyObservedObject) -> Bool {
return lhs.text == rhs.text
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.text)
}
static func newSet() -> [MyObservedObject] {
return [
MyObservedObject("a"),
MyObservedObject("b"),
MyObservedObject("c"),
]
}
}
class ContainerData: ObservableObject {
@Published var objects = MyObservedObject.newSet()
}
struct Container: View {
@ObservedObject private var data = ContainerData()
var body: some View {
ObjectVisualizer(data.objects)
Button("Click me") {
self.data.objects[2].text = "Changed"
}
}
}
struct ObjectVisualizer: View {
private var objects: [MyObservedObject]
init(_ objects: [MyObservedObject]) {
self.objects = objects
}
var body: some View {
ForEach(self.objects, id: \.self) {obj in
Text(obj.text)
}
}
}
struct ContentView: View {
var body: some View {
Container()
}
}
Upvotes: 0
Views: 1311
Reputation: 52397
Upfront caveat:
This would be way easier if your model were a struct rather than an ObservableObject -- you'd basically get all of this behavior for free. But, I'm assuming that there's a reason that your application can't be structured that way. That being said, trying to do this with the array of ObservableObjects is an uphill battle and I think is fighting against the way the SwiftUI is designed to work. However, this solution does appear to function.
import Combine
class MyObservedObject: Hashable, Equatable, ObservableObject {
let id = UUID()
@Published var text: String
init(_ text: String) {
self.text = text
}
static func == (lhs: MyObservedObject, rhs: MyObservedObject) -> Bool {
return lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(self.id)
}
static func newSet() -> [MyObservedObject] {
return [
MyObservedObject("a"),
MyObservedObject("b"),
MyObservedObject("c"),
]
}
}
class ContainerData: ObservableObject {
@Published var objects = MyObservedObject.newSet()
}
struct Container: View {
@ObservedObject private var data = ContainerData()
var body: some View {
VStack {
ObjectVisualizer(data.objects)
Button("Click me") {
self.data.objects[2].text = "Changed"
data.objectWillChange.send()
}
}
}
}
class MyObservedObjectWrapper : ObservableObject {
var objects: [MyObservedObject]
init(objects: [MyObservedObject]) {
self.objects = objects
}
}
struct ObjectVisualizer: View {
@ObservedObject var wrapper : MyObservedObjectWrapper
init(_ objects: [MyObservedObject]) {
self.wrapper = MyObservedObjectWrapper(objects: objects)
}
var body: some View {
ForEach(wrapper.objects, id: \.id) {obj in
Text(obj.text)
}
}
}
struct ContentView: View {
var body: some View {
Container()
}
}
MyObservedObject
has a unique ID, rather than relying on the text
field.
When the "Click me" button is pressed, it has to manually call objectWillChange.send()
since otherwise the ObservableObject doesn't know it's being updated (since you're modifying the array of reference types instead of value types.
The ObjectVisualizer
view didn't want to re-render with just the array of reference types being passed in. So, I put them in a wrapper object. This seems to trigger the re-render that was need in the ForEach
Is doing things like this worth not refactoring to have a struct model? Probably not, in my opinion, but of course, I don't have any more knowledge about how your particular situation works.
Upvotes: 2