Why an ObservedObject array is not updated in my SwiftUI application?

前端 未结 4 1459
被撕碎了的回忆
被撕碎了的回忆 2020-11-30 00:53

I\'m playing with SwitUI, trying to understand how ObservableObject works. I have an array of Person objects. When I add a new Person into the array, it is reloaded in my Vi

相关标签:
4条回答
  • 2020-11-30 01:25

    For those who might find it helpful. This is a more generic approach to @kontiki 's answer.

    This way you will not have to be repeating yourself for different model class types

    import Foundation
    import Combine
    import SwiftUI
    
    class ObservableArray<T>: ObservableObject {
    
        @Published var array:[T] = []
        var cancellables = [AnyCancellable]()
    
        init(array: [T]) {
            self.array = array
    
        }
    
        func observeChildrenChanges<T: ObservableObject>() -> ObservableArray<T> {
            let array2 = array as! [T]
            array2.forEach({
                let c = $0.objectWillChange.sink(receiveValue: { _ in self.objectWillChange.send() })
    
                // Important: You have to keep the returned value allocated,
                // otherwise the sink subscription gets cancelled
                self.cancellables.append(c)
            })
            return self as! ObservableArray<T>
        }
    
    
    }
    
    class Person: ObservableObject,Identifiable{
        var id: Int
        @Published var name: String
    
        init(id: Int, name: String){
            self.id = id
            self.name = name
        }
    
    } 
    
    struct ContentView : View {
        //For observing changes to the array only. 
        //No need for model class(in this case Person) to conform to ObservabeObject protocol
        @ObservedObject var mypeople: ObservableArray<Person> = ObservableArray(array: [
                Person(id: 1, name:"Javier"),
                Person(id: 2, name:"Juan"),
                Person(id: 3, name:"Pedro"),
                Person(id: 4, name:"Luis")])
    
        //For observing changes to the array and changes inside its children
        //Note: The model class(in this case Person) must conform to ObservableObject protocol
        @ObservedObject var mypeople: ObservableArray<Person> = try! ObservableArray(array: [
                Person(id: 1, name:"Javier"),
                Person(id: 2, name:"Juan"),
                Person(id: 3, name:"Pedro"),
                Person(id: 4, name:"Luis")]).observeChildrenChanges()
    
        var body: some View {
            VStack{
                ForEach(mypeople.array){ person in
                    Text("\(person.name)")
                }
                Button(action: {
                    self.mypeople.array[0].name="Jaime"
                    //self.mypeople.people.append(Person(id: 5, name: "John"))
                }) {
                    Text("Add/Change name")
                }
            }
        }
    }
    
    
    0 讨论(0)
  • 2020-11-30 01:29

    Person is a class, so it is a reference type. When it changes, the People array remains unchanged and so nothing is emitted by the subject. However, you can manually call it, to let it know:

    Button(action: {
        self.mypeople.objectWillChange.send()
        self.mypeople.people[0].name="Jaime"    
    }) {
        Text("Add/Change name")
    }
    

    Alternatively (and preferably), you can use a struct instead of a class. And you do not need to conform to ObservableObject, nor call .send() manually:

    import Foundation
    import SwiftUI
    import Combine
    
    struct Person: Identifiable{
        var id: Int
        var name: String
    
        init(id: Int, name: String){
            self.id = id
            self.name = name
        }
    
    }
    
    class People: ObservableObject{
        @Published var people: [Person]
    
        init(){
            self.people = [
                Person(id: 1, name:"Javier"),
                Person(id: 2, name:"Juan"),
                Person(id: 3, name:"Pedro"),
                Person(id: 4, name:"Luis")]
        }
    
    }
    
    struct ContentView: View {
        @ObservedObject var mypeople: People = People()
    
        var body: some View {
            VStack{
                ForEach(mypeople.people){ person in
                    Text("\(person.name)")
                }
                Button(action: {
                    self.mypeople.people[0].name="Jaime"
                }) {
                    Text("Add/Change name")
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-11-30 01:32

    I think there is a more elegant solution to this problem. Instead of trying to propagate the objectWillChange message up the model hierarchy, you can create a custom view for the list rows so each item is an @ObservedObject:

    struct PersonRow: View {
        @ObservedObject var person: Person
    
        var body: some View {
            Text(person.name)
        }
    }
    
    struct ContentView: View {
        @ObservedObject var mypeople: People
    
        var body: some View {
            VStack{
                ForEach(mypeople.people){ person in
                    PersonRow(person: person)
                }
                Button(action: {
                    self.mypeople.people[0].name="Jaime"
                    //self.mypeople.people.append(Person(id: 5, name: "John"))
                }) {
                    Text("Add/Change name")
                }
            }
        }
    }
    

    In general, creating a custom view for the items in a List/ForEach allows each item in the collection to be monitored for changes.

    0 讨论(0)
  • 2020-11-30 01:32

    ObservableArray is very useful, thank you! Here's a more generalised version that supports all Collections, which is handy when you need to react to CoreData values indirected through a to-many relationship (which are modelled as Sets).

    import Combine
    import SwiftUI
    
    private class ObservedObjectCollectionBox<Element>: ObservableObject where Element: ObservableObject {
        private var subscription: AnyCancellable?
        
        init(_ wrappedValue: AnyCollection<Element>) {
            self.reset(wrappedValue)
        }
        
        func reset(_ newValue: AnyCollection<Element>) {
            self.subscription = Publishers.MergeMany(newValue.map{ $0.objectWillChange })
                .eraseToAnyPublisher()
                .sink { _ in
                    self.objectWillChange.send()
                }
        }
    }
    
    @propertyWrapper
    public struct ObservedObjectCollection<Element>: DynamicProperty where Element: ObservableObject {
        public var wrappedValue: AnyCollection<Element> {
            didSet {
                if isKnownUniquelyReferenced(&observed) {
                    self.observed.reset(wrappedValue)
                } else {
                    self.observed = ObservedObjectCollectionBox(wrappedValue)
                }
            }
        }
        
        @ObservedObject private var observed: ObservedObjectCollectionBox<Element>
    
        public init(wrappedValue: AnyCollection<Element>) {
            self.wrappedValue = wrappedValue
            self.observed = ObservedObjectCollectionBox(wrappedValue)
        }
        
        public init(wrappedValue: AnyCollection<Element>?) {
            self.init(wrappedValue: wrappedValue ?? AnyCollection([]))
        }
        
        public init<C: Collection>(wrappedValue: C) where C.Element == Element {
            self.init(wrappedValue: AnyCollection(wrappedValue))
        }
        
        public init<C: Collection>(wrappedValue: C?) where C.Element == Element {
            if let wrappedValue = wrappedValue {
                self.init(wrappedValue: wrappedValue)
            } else {
                self.init(wrappedValue: AnyCollection([]))
            }
        }
    }
    

    It can be used as follows, let's say for example we have a class Fridge that contains a Set and our view needs to react to changes in the latter despite not having any subviews that observe each item.

    class Food: ObservableObject, Hashable {
        @Published var name: String
        @Published var calories: Float
        
        init(name: String, calories: Float) {
            self.name = name
            self.calories = calories
        }
        
        static func ==(lhs: Food, rhs: Food) -> Bool {
            return lhs.name == rhs.name && lhs.calories == rhs.calories
        }
        
        func hash(into hasher: inout Hasher) {
            hasher.combine(self.name)
            hasher.combine(self.calories)
        }
    }
    
    class Fridge: ObservableObject {
        @Published var food: Set<Food>
        
        init(food: Set<Food>) {
            self.food = food
        }
    }
    
    struct FridgeCaloriesView: View {
        @ObservedObjectCollection var food: AnyCollection<Food>
    
        init(fridge: Fridge) {
            self._food = ObservedObjectCollection(wrappedValue: fridge.food)
        }
    
        var totalCalories: Float {
            self.food.map { $0.calories }.reduce(0, +)
        }
    
        var body: some View {
            Text("Total calories in fridge: \(totalCalories)")
        }
    }
    
    0 讨论(0)
提交回复
热议问题