为什么我的SwiftUI应用程序中未更新ObservedObject数组?

jar*_*aez 9 swiftui

我正在玩SwitUI,试图了解ObservableObject的工作原理。我有一个Person对象数组。当我将新的Person添加到数组中时,它将重新加载到我的View中。但是,如果我更改现有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")]
    }

}
Run Code Online (Sandbox Code Playgroud)
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")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

恐怕我做错了什么,或者我不知道ObservedObjects如何与Arrays一起工作。

欢迎任何帮助或解释!

小智 36

我认为这个问题有一个更优雅的解决方案。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")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 抱歉,投了反对票。嵌套 ObservableObjects 的问题是,当一个人的名字更改时,列表无法正确更新。最好使用结构对数据进行建模,以便列表知道在发生这种情况时进行更新。您可能不会立即遇到问题,但当您尝试实施过滤时就会遇到问题。 (4认同)
  • 谢谢,这正是我一直在寻找的。这是我见过的唯一解决方案,它允许您通过改变集合中给定引用的属性来触发重新渲染,而无需对集合本身进行操作(使用按索引访问或其他方式)。例如,这允许将 ObservableObjects 数组中的随机元素存储在变量中,并通过仅对该变量进行操作来触发重新渲染。 (3认同)
  • 我认为这应该是公认的答案。WWDC20 的许多 swiftui 数据相关视频也推荐了这种方法。我认为要么使用所有结构方法,即传递索引和标识符(相信我,很难从绑定数组中获取经过过滤的绑定数组!),要么使用所有 ObservableObjects 进行数据建模并正确分离视图。 (2认同)

kon*_*iki 21

Person是一个类,因此它是一个引用类型。当它改变时,People数组保持不变,因此主题没有发出任何东西。但是,您可以手动调用它,以使其知道:

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

另外(最好是),您可以使用结构代替类。而且您不需要遵循ObservableObject,也不需要手动调用.send():

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")
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 修改了答案,包括另一个选项(使用结构,而不是类) (3认同)
  • 谢谢!!很好地解释和理解。两种解决方案都有效,但我将按照您的建议使用结构而不是类。更干净了。 (2认同)
  • 为什么 `self.mypeople.objectWillChange.send()` 必须放在 `self.mypeople.people[0].name="Jaime" ` 之前?以相反的方式做更有意义。@kon (2认同)

Net*_*rks 13

对于那些可能会发现它有帮助的人。这是@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")
            }
        }
    }
}

Run Code Online (Sandbox Code Playgroud)

  • 很好,谢谢!对于那些尝试测试这一点的人来说,有一个小错字:self.mypeople.people 应该是 self.mypeople.array (2认同)

小智 7

理想的做法是链接@ObservedObject@StateObject以及一些其他适合序列的属性包装器,例如@StateObject @ObservableObjects. 但是您不能使用多个属性包装器,因此您需要创建不同的类型来处理两种不同的情况。然后您可以根据需要使用以下任一选项。

\n

(你的People类型是不必要的\xe2\x80\x94它的目的可以抽象为所有序列。)

\n
@StateObjects var people = [\n  Person(id: 1, name:"Javier"),\n  Person(id: 2, name:"Juan"),\n  Person(id: 3, name:"Pedro"),\n  Person(id: 4, name:"Luis")\n]\n\n@ObservedObjects var people: [Person]\n
Run Code Online (Sandbox Code Playgroud)\n
import Combine\nimport SwiftUI\n\n@propertyWrapper\npublic final class ObservableObjects<Objects: Sequence>: ObservableObject\nwhere Objects.Element: ObservableObject {\n  public init(wrappedValue: Objects) {\n    self.wrappedValue = wrappedValue\n    assignCancellable()\n  }\n\n  @Published public var wrappedValue: Objects {\n    didSet { assignCancellable() }\n  }\n\n  private var cancellable: AnyCancellable!\n}\n\n// MARK: - private\nprivate extension ObservableObjects {\n  func assignCancellable() {\n    cancellable = Publishers.MergeMany(wrappedValue.map(\\.objectWillChange))\n      .sink { [unowned self] _ in objectWillChange.send() }\n  }\n}\n\n\n// MARK: -\n\n@propertyWrapper\npublic struct ObservedObjects<Objects: Sequence>: DynamicProperty\nwhere Objects.Element: ObservableObject {\n  public init(wrappedValue: Objects) {\n    _objects = .init(\n      wrappedValue: .init(wrappedValue: wrappedValue)\n    )\n  }\n\n  public var wrappedValue: Objects {\n    get { objects.wrappedValue }\n    nonmutating set { objects.wrappedValue = newValue }\n  }\n\n  public var projectedValue: Binding<Objects> { $objects.wrappedValue }\n\n  @ObservedObject private var objects: ObservableObjects<Objects>\n}\n\n@propertyWrapper\npublic struct StateObjects<Objects: Sequence>: DynamicProperty\nwhere Objects.Element: ObservableObject {\n  public init(wrappedValue: Objects) {\n    _objects = .init(\n      wrappedValue: .init(wrappedValue: wrappedValue)\n    )\n  }\n\n  public var wrappedValue: Objects {\n    get { objects.wrappedValue }\n    nonmutating set { objects.wrappedValue = newValue }\n  }\n\n  public var projectedValue: Binding<Objects> { $objects.wrappedValue }\n\n  @StateObject private var objects: ObservableObjects<Objects>\n}\n
Run Code Online (Sandbox Code Playgroud)\n


Luk*_*ard 6

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([]))
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

它可以按如下方式使用,例如,我们有一个类 Fridge,其中包含一个 Set,并且我们的视图需要对后者的更改做出反应,尽管没有任何子视图来观察每个项目。

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)")
    }
}
Run Code Online (Sandbox Code Playgroud)