Why does SwiftUI sidebar not update when ObservedObject changes?

I have a simple app with a sidebar listing items and an edit view to edit the details of the selected item.

One of the parameters that can be changed in the edit view is the name of the item that is also shown in the sidebar.

Below is a simple app demonstrating the problem. It lits 4 people and you can change the name. When you change the name, the sidebar does not change until another is selected.

I would like the name in the sidebar to change as it is edited. If I added a text view in the edit view, then it would change as the TextField changes. Why does the item in the sidebar not change (or only change when selecting different item)?

class Person: NSObject, ObservableObject {
  @Published public var name: String
  
  init(name: String) {
    self.name = name
  }
}

class Database {
  public var people: [Person]
  
  init() {
    people = [Person(name: "Susan"), Person(name: "John"),
              Person(name: "Jack"), Person(name: "Mary")]
  }
}

@main
struct BindingTestApp: App {
  @State private var database = Database(
  
  var body: some Scene {
    WindowGroup {
      ContentView(people: $database.people)
    }
  }
}

struct ContentView: View {
  struct SidebarView: View {
    @Binding var people: [Person]
    @Binding var selectedPerson: Person?
    
    var body: some View {
      List(people, id: \.self, selection: $selectedPerson) { person in
        Text(person.name)
      }.listStyle(SidebarListStyle())
    }
  }
 
  struct PersonView: View {
    @ObservedObject public var person: Person
    
    var body: some View {
      TextField("Name", text: $person.name)
    }
  }
  
  @Binding var people: [Person]
  @State private var selectedPerson: Person?
  
  var body: some View {
    NavigationView {
      SidebarView(people: $people, selectedPerson: $selectedPerson)
      if let selectedPerson = selectedPerson {
        PersonView(person: selectedPerson)
      } else {
        Text("Please select or create a person.")
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  @State static private var database = Database()
  
  static var previews: some View {
    ContentView(people: $database.people)
  }
}

Accepted Reply

The problem is that person is never observed. The Text view in the list will not be notified when the person's name changes because it is not observed.

To fix the problem, create another view that observes the class (ObservableObject). That entire view is invalidated when the @Published field in the @ObservedObject changes.

The code below fixes the problem and the sidebar is correctly updated in real time as the name is edited.

class Person: NSObject, ObservableObject {
  @Published public var name: String
  
  init(name: String) {
    self.name = name
  }
}

class Database {
  public var people: [Person]
  
  init() {
    people = [Person(name: "Susan"), Person(name: "John"),
              Person(name: "Jack"), Person(name: "Mary")]
  }
}

@main
struct BindingTestApp: App {
  @State private var database = Database()
  
  var body: some Scene {
    WindowGroup {
      ContentView(people: $database.people)
    }
  }
}

struct PersonView: View {
  @ObservedObject var person: Person
  
  var body: some View {
    Text(person.name)
  }
}

struct ContentView: View {
  struct SidebarView: View {
    @Binding var people: [Person]
    @Binding var selectedPerson: Person?
    
    var body: some View {
      List(people, id: \.self, selection: $selectedPerson) { person in
        PersonView(person: person)
      }.listStyle(SidebarListStyle())
    }
  }
  
  struct PersonView: View {
    @ObservedObject public var person: Person
    
    var body: some View {
      TextField("Name", text: $person.name)
    }
  }
  
  @Binding var people: [Person]
  @State private var selectedPerson: Person?
  
  var body: some View {
    NavigationView {
      SidebarView(people: $people, selectedPerson: $selectedPerson)
      if let selectedPerson = selectedPerson {
        PersonView(person: selectedPerson)
      } else {
        Text("Please select or create a person.")
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  @State static private var database = Database()
  
  static var previews: some View {
    ContentView(people: $database.people)
  }
}

Replies

The problem is that person is never observed. The Text view in the list will not be notified when the person's name changes because it is not observed.

To fix the problem, create another view that observes the class (ObservableObject). That entire view is invalidated when the @Published field in the @ObservedObject changes.

The code below fixes the problem and the sidebar is correctly updated in real time as the name is edited.

class Person: NSObject, ObservableObject {
  @Published public var name: String
  
  init(name: String) {
    self.name = name
  }
}

class Database {
  public var people: [Person]
  
  init() {
    people = [Person(name: "Susan"), Person(name: "John"),
              Person(name: "Jack"), Person(name: "Mary")]
  }
}

@main
struct BindingTestApp: App {
  @State private var database = Database()
  
  var body: some Scene {
    WindowGroup {
      ContentView(people: $database.people)
    }
  }
}

struct PersonView: View {
  @ObservedObject var person: Person
  
  var body: some View {
    Text(person.name)
  }
}

struct ContentView: View {
  struct SidebarView: View {
    @Binding var people: [Person]
    @Binding var selectedPerson: Person?
    
    var body: some View {
      List(people, id: \.self, selection: $selectedPerson) { person in
        PersonView(person: person)
      }.listStyle(SidebarListStyle())
    }
  }
  
  struct PersonView: View {
    @ObservedObject public var person: Person
    
    var body: some View {
      TextField("Name", text: $person.name)
    }
  }
  
  @Binding var people: [Person]
  @State private var selectedPerson: Person?
  
  var body: some View {
    NavigationView {
      SidebarView(people: $people, selectedPerson: $selectedPerson)
      if let selectedPerson = selectedPerson {
        PersonView(person: selectedPerson)
      } else {
        Text("Please select or create a person.")
      }
    }
  }
}

struct ContentView_Previews: PreviewProvider {
  @State static private var database = Database()
  
  static var previews: some View {
    ContentView(people: $database.people)
  }
}