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.
A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
Open Xcode and either click Create a new Xcode project in Xcode's startup window, or choose File > New > Project. In the template selector, select iOS as the platform, select the Single View App template, and then click Next.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With