为什么在我的 SwiftUI 应用程序中没有更新观察对象数组?

我正在玩 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(约翰)行,Jaime 的名称显示正确,但如果我只是更改名称,这不会显示在视图中。

我担心我做错了什么,或者我不知道 ObservedObjects是如何处理数组的。

51194 次浏览

You can use a struct instead of a class. Because of a struct's value semantics, a change to a person's name is seen as a change to Person struct itself, and this change is also a change to the people array so @Published will send the notification and the View body will be recomputed.

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

Alternatively (and not recommended), 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")
}

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


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.

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

The ideal thing to do would be to chain @ObservedObject or @StateObject and some other property wrapper that is suitable for sequences, e.g. @StateObject @ObservableObjects. But you can't use more than one property wrapper, so you need to make different types to handle the two different cases. Then you can use either one of the following, as appropriate.

(Your People type is unnecessary—its purpose can be abstracted to all sequences.)

@StateObjects var people = [
Person(id: 1, name:"Javier"),
Person(id: 2, name:"Juan"),
Person(id: 3, name:"Pedro"),
Person(id: 4, name:"Luis")
]


@ObservedObjects var people: [Person]
import Combine
import SwiftUI


@propertyWrapper
public final class ObservableObjects<Objects: Sequence>: ObservableObject
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
self.wrappedValue = wrappedValue
assignCancellable()
}


@Published public var wrappedValue: Objects {
didSet { assignCancellable() }
}


private var cancellable: AnyCancellable!
}


// MARK: - private
private extension ObservableObjects {
func assignCancellable() {
cancellable = Publishers.MergeMany(wrappedValue.map(\.objectWillChange))
.sink { [unowned self] _ in objectWillChange.send() }
}
}




// MARK: -


@propertyWrapper
public struct ObservedObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}


public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}


public var projectedValue: Binding<Objects> { $objects.wrappedValue }


@ObservedObject private var objects: ObservableObjects<Objects>
}


@propertyWrapper
public struct StateObjects<Objects: Sequence>: DynamicProperty
where Objects.Element: ObservableObject {
public init(wrappedValue: Objects) {
_objects = .init(
wrappedValue: .init(wrappedValue: wrappedValue)
)
}


public var wrappedValue: Objects {
get { objects.wrappedValue }
nonmutating set { objects.wrappedValue = newValue }
}


public var projectedValue: Binding<Objects> { $objects.wrappedValue }


@StateObject private var objects: ObservableObjects<Objects>
}