Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

swiftui

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.

like image 858
jarnaez Avatar asked Aug 12 '19 10:08

jarnaez


People also ask

What is Observedobject Swiftui?

A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.

How do I create an Observableobject in Swiftui?

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.


2 Answers

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") } 
like image 196
kontiki Avatar answered Oct 08 '22 06:10

kontiki


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.

like image 29
Stuart Malone Avatar answered Oct 08 '22 06:10

Stuart Malone