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

I'm playing with SwiftUI, 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 View, however if I change the value of an existing Person, it is not reloaded in the View.

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

If I uncomment the line to add a new Person (John), the name of Jaime is shown properly, however if I just change the name this is not shown in the View.

I'm afraid I'm doing something wrong or maybe I don't get how the ObservedObjects work with arrays.


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

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.


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


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