【问题标题】:Why is an ObservedObject array not updated in my SwiftUI application?为什么我的 SwiftUI 应用程序中没有更新 ObservedObject 数组?
【发布时间】:2019-12-18 22:43:36
【问题描述】:

我正在使用 SwiftUI,试图了解 ObservableObject 的工作原理。我有一组 Person 对象。当我将新的Person 添加到数组中时,它会重新加载到我的视图中,但是如果我更改现有Person 的值,它不会重新加载到视图中。

//  NamesClass.swift
import Foundation
import SwiftUI
import Combine

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 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
    
    var body: some View {
        VStack{
            ForEach(mypeople.people){ person in
                Text("\(person.name)")
            }
            Button(action: {
                self.mypeople.people[0].name="Jaime"
                //self.mypeople.people.append(Person(id: 5, name: "John"))
            }) {
                Text("Add/Change name")
            }
        }
    }
}

如果我取消注释该行以添加新的Person (John),Jaime 的名字会正确显示,但是如果我只是更改名称,则视图中不会显示。

恐怕我做错了什么,或者我不明白ObservedObjects 是如何处理数组的。

【问题讨论】:

    标签: swiftui


    【解决方案1】:

    您可以使用结构而不是类。由于结构体的值语义,对人名的更改被视为对 Person 结构体本身的更改,并且此更改也是对人员数组的更改,因此@Published 将发送通知并重新计算视图主体。

    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 Model: 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 {
        @StateObject var model = Model()
    
        var body: some View {
            VStack{
                ForEach(model.people){ person in
                    Text("\(person.name)")
                }
                Button(action: {
                    self.mypeople.people[0].name="Jaime"
                }) {
                    Text("Add/Change name")
                }
            }
        }
    }
    

    或者(不推荐),Person 是一个类,所以它是一个引用类型。当它发生变化时,People 数组保持不变,因此主题不会发出任何内容。但是,您可以手动调用它,让它知道:

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

    【讨论】:

    • 修改了答案,包括另一个选项(使用结构,而不是类)
    • 谢谢!!很好的解释和理解。两种解决方案都有效,但我将按照您的建议使用结构而不是类。它更干净。
    • @kontiki 很好的答案。我一直在尝试为 NSManagedObject 的子类做同样的事情,但没有成功。有什么建议?谢谢。
    • 嗨@ChuckH。老实说,我还没有用 CoreData + SwiftUI 做太多工作。但是,如果您认为它不会被标记为重复,也许您可​​以发布另一个问题并详细说明您面临的挑战。如果不是我,肯定有人可以提供帮助。虽然我也会尝试一下;-) 干杯。
    • 为什么self.mypeople.objectWillChange.send()必须放在self.mypeople.people[0].name="Jaime" 之前?以相反的方式做更有意义。 @kon
    【解决方案2】:

    我认为这个问题有一个更优雅的解决方案。您可以为列表行创建一个自定义视图,而不是尝试将 objectWillChange 消息传播到模型层次结构中,这样每个项目都是一个 @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")
                }
            }
        }
    }
    

    通常,为 List/ForEach 中的项目创建自定义视图可以监控集合中的每个项目的更改。

    【讨论】:

    • 谢谢,这正是我想要的。这是我见过的唯一一种解决方案,它允许您通过改变集合中给定引用的属性来触发重新渲染,而无需对集合本身进行操作(使用按索引访问或其他方式)。例如这允许将 ObservableObjects 数组中的随机元素存储在变量中,并通过仅对该变量进行操作来触发重新渲染。
    • 同意,这不需要传递索引或某种奇怪的巫术,并有助于将每个视图分解为单独的实体。
    • 我认为这应该是公认的答案。而且WWDC20上的很多swiftui数据相关视频也推荐了这种方式。我认为要么您使用所有结构方法,即传递索引和标识符(很难从绑定数组中获得过滤的绑定数组,相信我!),或者使用所有 ObservableObjects 进行数据建模并正确分离您的视图.
    • 它几乎破坏了我在结构方法中使用的所有代码的完整性,并且不得不编写大量胶水代码。例如,我所有视图的初始化程序几乎都是手写的,以正确初始化视图的绑定。还有很多 Binding(get:{}, set{}),更糟糕的是例如 .sheet(isPresented: Binding(get:{}, set{})){ SomeView(......)) .在处理数据集合和嵌套结构时,@Binding 确实不够成熟。
    • 投反对票对不起。嵌套 ObservableObjects 的问题是当一个人的名字被更改时,列表不会正确更新。最好使用结构对数据进行建模,以便列表在发生这种情况时知道更新。您可能不会立即遇到问题,但当您尝试实施过滤时就会遇到问题。
    【解决方案3】:

    对于那些可能会觉得有帮助的人。这是@kontiki 答案的更通用方法。

    这样您就不必为不同的模型类类型重复自己

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

    【讨论】:

    • 很好,谢谢!对于那些试图测试这一点的人,有一个小错字:self.mypeople.people 应该是 self.mypeople.array
    • 感谢您指出这一点 - 我刚刚提交了修改以修复错字
    【解决方案4】:

    ObservableArray 非常有用,谢谢!这是一个更通用的版本,它支持所有集合,当您需要对通过一对多关系(被建模为集合)间接的 CoreData 值做出反应时,这很方便。

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

    它可以如下使用,例如我们有一个包含 Set 的类 Fridge,尽管没有观察每个项目的任何子视图,但我们的视图需要对后者的变化做出反应。

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

    【讨论】:

    • 不确定使用@StateObject 拥有 ObservedObjectCollectionBox 是否更好,我假设不是因为它不是新的事实来源,但欢迎提出建议。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多