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

时间:2019-08-12 10:43:10

标签: 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")]
    }

}
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(约翰)这一行,那么Jaime的名称将正确显示。但是,如果仅更改名称,则视图中不会显示。

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

欢迎任何帮助或解释!

4 个答案:

答案 0 :(得分:24)

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

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

或者(最好是),您可以使用结构而不是类。而且您不需要遵循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")
            }
        }
    }
}

答案 1 :(得分:2)

对于那些可能会有所帮助的人。这是@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<K>(_ type:K.Type) throws ->ObservableArray<T> where K : ObservableObject{
        let array2 = array as! [K]
        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
    }

}

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(Person.self)

    var body: some View {
        VStack{
            ForEach(mypeople.array){ 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")
            }
        }
    }
}

答案 2 :(得分:1)

我认为有一个更优雅的解决方案。您可以尝试为列表行创建一个自定义视图,以便每个项目都是一个@ObservedObject:

,而不是尝试在模型层次结构中传播objectWillChange消息。
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中的项目创建自定义视图,可以监视集合中的每个项目的更改。

答案 3 :(得分:0)

ObservableArray非常有用,谢谢!这是支持所有Collections的更通用的版本,当您需要对通过多对多关系(建模为Sets)间接访问的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)")
    }
}