Home > Back-end >  View not updating for modified child of ObservedObject
View not updating for modified child of ObservedObject

Time:11-11

I'm trying to build a View which a list of children of an object, and I want those children to be modifiable. The data changes, but the view doesn't react.

Here is the stripped down example.

class ObjectOne: ObservableObject {
    
   @Published var children: [ObjectTwo]
    
    init() {
        self.children = []
    }
}
class ObjectTwo: ObservableObject {
    
    @Published var value: Value
    var id = UUID()
    
    init(value: Value) {
        self.value = value
    }
}
struct ContentView: View {
    
    @ObservedObject var object: ObjectOne = ObjectOne()
    
    var body: some View {
        VStack {
            Button {
                self.object.children.append(ObjectTwo(value: .one))
            } label: {
                Text("Add Object")
            }
            Text("Objects:")
            ForEach(self.object.children, id: \.id) { object in
                HStack {
                    Text(String(object.value.rawValue))
                    Spacer()
                    Button {
                        if object.value == .one {
                            object.value = .two
                        } else {
                            object.value = .one
                        }
                    } label: {
                        Text("Toggle")
                    }
                    
                }
            }
        }
    }
}

Adding objects works and the list updates, but modifying the value of the ObjectTwo doesn't update the view.

CodePudding user response:

When you have nested ObservableObject, you need to manually tell them about upcoming changes. Try this, works for me.

Button {
    self.object.objectWillChange.send()  // <--- here
    if object.value == .one {
        object.value = .two
    } else {
        object.value = .one
    }
} label: {
    Text("Toggle")
}

I recommend you use a struct for your ObjectTwo, it is easier to use (with some minor code change) and IMHO common practice.

For example, you could use this:

struct ObjectTwo: Identifiable {  // <-- here
    var value: Value
    var id = UUID()
    
    init(value: Value) {
        self.value = value
    }
}

struct ContentView: View {
    @StateObject var objectModel = ObjectOne() // <-- here
    
    var body: some View {
        VStack {
            Button {
                objectModel.children.append(ObjectTwo(value: .one))
            } label: {
                Text("Add Object")
            }
            Text("Objects:")
            ForEach($objectModel.children, id: \.id) { $object in  // <-- here
                HStack {
                    Text(String(object.value.rawValue))
                    Spacer()
                    Button {
                        if object.value == .one {
                            object.value = .two
                        } else {
                            object.value = .one
                        }
                    } label: {
                        Text("Toggle")
                    }
                }
            }
        }
    }
}

CodePudding user response:

Swift is designed to use value types instead of objects (to prevent consistency bugs) see Choosing Between Structures and Classes - Apple Developer. SwiftUI exploits these value semantics in its implementation of dependency and change tracking, if you use objects you'll lose these features. Generally in SwiftUI we can get away with one store object (that persists/syncs model data) and use model structs so we can take advantage of SwiftUI's dependency tracking of value types and @Binding for write access, e.g.

class Store: ObservableObject {
   static var shared = Store()
   static var preview = Store(preview: true)

   @Published var items: [Item] = []
    
    init(preview: Bool) {
        if (preview) {
            items = [Item(text: "Test")]
        }
        else {
           load()
        }
    }

    func load(){}
    func save(){}
}

class Item: Identifiable {
    let id = UUID()
    @Published title = ""
    
    init(title: String) {
        self.title = title
    }
}

@main
struct TestApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
            .environmentObject(Store.shared)
        }
    }
}

struct ContentView: View {
    @EnvironmentObject var store: Store
    var body: View {
        List($store.items) { $item in
            TextField("Title", text: $item.title)
        }
     }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
        .environmentObject(Store.preview)
    }
}
  • Related