Detail View is not updated when the model is updated (Using List) SwiftUI

Viewed 442

I am using List to show the models in the main view. When I update the model in detail view it is not updated in detail view.

When I don't use List, detail view is updated. What am I missing for List?


struct Person: Identifiable {
  var id: UUID
  var name: String
}

class PersonModel: ObservableObject {
  @Published var persons: [Person] = [Person(id: UUID(), name: "Ege")]
}

struct PersonListView: View {
  
  @StateObject private var personModel = PersonModel()
  
  var body: some View {
    NavigationView {
      List {
        ForEach(personModel.persons) { person in
          NavigationLink(destination: PersonDetailView(person: person).environmentObject(personModel)) {
            Text(person.name)
          }
        }
      }
      .navigationTitle("Persons")
    }
  }
}

struct PersonDetailView: View {
  
  let person: Person
  @EnvironmentObject var personModel: PersonModel
  
  var body: some View {
    VStack {
      Text(person.name)
      
      Button(action: {
        let personIndex = personModel.persons.firstIndex(where: { $0.id == person.id })!
        personModel.persons[personIndex].name = "Updated Name"
      }) {
        Text("Update")
      }
    }
    .navigationTitle("Person Detail")
  }
}

Workarounds that I used for List:

  • Using custom binding function for NavigationLink.

Example code:

//1
private func binding(for person: Person) -> Binding<Person> {
  let personIndex = personModel.persons.firstIndex(where: { $0.id == person.id }) ?? 0
  return $personModel.persons[personIndex]
}
//2
NavigationLink(destination: PersonDetailView(person: binding(for: person)))
//3
@Binding var person: Person //DetailView
  • Using another @State in detail view which is initialized with passed model in onAppear method.
3 Answers

This issue seems to be fixed in Xcode 13 Beta.

It seems to me that, although your workaround surely works, you'd like to know what's happening and why you need a workaround in the first place.

When you update the value of your @Published var persons: [Person] array, your detail view is re-rendered as you would expect. But you are telling your view to print the name value of the var person: Person struct which hasn't changed. It's still the same as when the view was first created.

If you replace

Text(person.name)

with

Text(personModel.persons.first(where: { $0.id == person.id } )!.name)

it updates as you would expect, because you're providing the Text view with the name value of the new version of person pulled from your actual PersonModel that you just updated.

So the involvement of List doesn't seem to be the issue, which is why I haven't directly answered your question "What am I missing for List?". Hopefully it helps anyway!

EDIT

You can send manual changes but you need to use class instead of struct for the model. Like this

Button(action: {
    if let personIndex = personModel.persons.firstIndex(where: { $0.id == person.id }) { //<==Here
        personModel.persons[personIndex].name = "Updated Name"
        personModel.objectWillChange.send() //<==Here
    }
    
})

The possible solution is to update your data and reassign elements to the array.

struct PersonDetailView: View {
    
    let person: Person
    @EnvironmentObject var personModel: PersonModel
    
    var body: some View {
        VStack {
            Text(person.name)
            
            Button(action: {
                if let personIndex = personModel.persons.firstIndex(where: { $0.id == person.id }) { //<==Here
                    personModel.persons[personIndex].name = "Updated Name"
                    personModel.persons[personIndex] = personModel.persons[personIndex]
                }
                
            }) {
                Text("Update")
            }
        }
        .navigationTitle("Person Detail")
    }
}

And use class

class Person: Identifiable {
    var id: UUID
    var name: String
    
    init(id: UUID, name: String) {
        self.id = id
        self.name = name
    }
}

Related