pistacchio
pistacchio

Reputation: 58903

Detect changes in an ObservableObject array of ObservableObjects

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:

  1. Have a view (Container) with an observable array of observable objects
  2. Access one of the objects by its index in the array
  3. Change a property of the observed object instance
  4. Have a child view (ObjectVisualizer) with the passed data reflect the change done to the object by redrawing the array with the changed property

The 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

Answers (1)

jnpdx
jnpdx

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()
    }
}

What's happening:

  1. MyObservedObject has a unique ID, rather than relying on the text field.

  2. 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.

  3. 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

Related Questions