Как обновить список в SwiftUI с помощью модели @Observable

Я пытаюсь понять, почему мои представления строк не обновляются, когда я повторно заполняю массив, содержащий их.

В качестве примера у меня есть следующие мнения.

//main content view which generates a list full of PersonRow
struct ContentView: View {
  @State private var org : Organization = .shared
  
  var body: some View {
    List(org.people) {
      PersonRow(person: $0)
    }
    .padding()
    
    Button(action: {
      org.createPeople()
    }) {
      Text("Create People")
    }
  }
}
//Row representing each person.
struct PersonRow : View {
  @State var person : Person //observable
  
  var body: some View {
    let color : Color = person.isActive ? .blue : .red
    HStack {
      
    Text(person.name)
      .foregroundStyle(color)
      
      Spacer()
      
      Button(action: {
        person.isActive.toggle()
      }) {
        Text(person.isActive ? "Active" : "Inactive")
      }
    }
    .padding()
  }
}
@Observable class Person : Identifiable, Equatable {
  static func == (lhs: Person, rhs: Person) -> Bool { lhs.id == rhs.id }
  
  var id : String = ""
  var name: String = "John Doe"
  var isActive: Bool = true
}
@Observable class Organization {
  
  static let shared = Organization()
  
  private init() { createPeople() }
  
  var people: [Person] = []
  
  func createPeople() {
    people.removeAll()
    
    for i in 0..<10 {
      let p = Person()
      p.id = "\(i)"
      p.name = "John Doe \(i)"
      people.append(p)
    }
    
    print("Created people")
  }
}

В каждой строке при нажатии «активный» или «неактивный» он переключается, как и ожидалось, и представление обновляется, как и ожидалось.

Однако когда я нажимаю «Создать людей» в ContentView, я повторно заполняю весь массив «Организация» новым набором «Лицо», но список не обновляется. Вот здесь у меня возникла проблема. Разве список не должен обновиться, поскольку я наблюдаю Oragnization и массив был обновлен? Может кто-нибудь объяснить, что я делаю неправильно?

State создает глубокую копию Observable, которую вместо этого следует использовать @Bindable. Также обратите внимание, что State всегда должно быть private. Эта реализация Equatable также сообщает SwiftUI обновляться только при изменении id, вам необходимо включить все свойства для Equatable.
lorem ipsum 14.08.2024 18:12

@loremipsum Спасибо! Я работал над этим несколько дней и почему-то пропустил ни одной ссылки на @Bindable. Есть ли способ сделать это, сохраняя Equatable и полагаясь только на идентификатор? У меня есть причина для этой конкретной установки.

MAH 14.08.2024 18:38

Нет, Equatable используется SwiftUI, чтобы узнать, изменилось ли что-то. Если во внимание принимается только идентификатор, то только изменение идентификатора вызовет изменение.

lorem ipsum 14.08.2024 18:44

Спасибо! Я бы отметил ваш ответ как правильный, но сделать это в виде комментария невозможно. Не стесняйтесь оставить ответ в любое время.

MAH 14.08.2024 19:05
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
50
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

State создает глубокую копию Observable, которую вместо этого следует использовать @Bindable. Также обратите внимание, что State всегда должно быть private.

Эта реализация Equatable также сообщает SwiftUI обновляться только при изменении id, вам необходимо включить все свойства для Equatable.

Этот ответ объясняет, что я делаю неправильно. Последующий вопрос. Есть ли способ сказать самому списку воссоздать все его строки? Я знаю, что нет ничего похожего на reloadData, но какой-нибудь трюк, чтобы заставить это обновление?

MAH 14.08.2024 19:11

@MAH Вы можете дать списку «идентификатор» и изменить его. Это решение ужасно с точки зрения эффективности: оно все воссоздает.

lorem ipsum 14.08.2024 19:13

Понятно. Согласитесь, не лучший подход. Я постараюсь обойти Equatable. Время реструктуризации. Еще раз спасибо за вашу помощь.

MAH 14.08.2024 19:25

@MAH Я только что добавил ответ, который может дать некоторое представление обо всем. Вам не нужно присваивать списку идентификатор, поскольку он автоматически обновляется при изменении данных. Так что, возможно, прочтите это, прежде чем все реструктурировать.

Andrei G. 14.08.2024 21:36

Теперь это просто let для @Observable class модельных объектов (раньше нам требовался @ObservedObject var), например.

struct ContentView: View {
    let org = Organization.shared // singleton, usually called Store that loads/saves models

И

struct PersonRow : View {
    let person: Person // @Observable owned by the store

@State предназначен для значений данных представления или структур, принадлежащих представлению, а не для моделей.

См. 9:40 в этом видео, чтобы узнать о разнице между данными представления и данными модели. https://developer.apple.com/wwdc20/10040?time=580

Но State предназначен для временного состояния пользовательского интерфейса, локального для представления. В этом разделе я хочу обратить ваше внимание на разработку вашей модели и объяснить все инструменты, которые SwiftUI предоставляет вам.

Обычно в вашем приложении вы храните и обрабатываете данные, используя модель данных, отдельную от пользовательского интерфейса.

Это когда вы достигаете критической точки, когда вам необходимо управлять жизненным циклом ваших данных, включая их сохранение и синхронизацию, обработку побочных эффектов и, в более общем плане, интеграцию их с существующими компонентами. Именно здесь вам следует использовать ObservableObject. Во-первых, давайте посмотрим, как определяется ObservableObject.

Как отметил @loremipsum в комментариях, учитывая протокол Equatable, значение id имеет важное значение.

Проблема в том, что ваша функция createPeople() воссоздает одних и тех же людей - с тем же именем и тем же идентификатором (от 0 до 10). Итак, технически никаких изменений нет.

Если вы рандомизируете идентификатор или если фактические данные будут содержать разные идентификаторы, все должно работать нормально:

func createPeople() {
        people.removeAll()
        
        for i in 0..<10 {
            let p = Person()
            p.id = "\(UUID())" // <-- HERE, randomize id
            p.name = "John Doe \( p.id.suffix(4) )" // <-- HERE, show the last 4 characters of the UUID as part of the name
            people.append(p)
        }
        
        print("Created people")
    }

Однако обратите внимание, что если какой-либо из новых идентификаторов НЕ изменится, статус человека будет сохранен/запомнен. См. ниже, учитывая более ограниченную рандомизацию, когда вы нажимаете, чтобы отключить некоторых людей и создать новых, если кто-либо из новых имеет тот же идентификатор, статус не будет сброшен:

func createPeople() {
        people.removeAll()
            
        for i in 0..<10 {
            let p = Person()
            p.id = "\(Int.random(in: 0..<50))" // <-- HERE, randomize id
            p.name = "John Doe \(p.id)" // <-- HERE, show the id as part of the name
            people.append(p)
        }
        
        print("Created people")
    }

Кстати, State здесь должно работать нормально. Так и должно быть Bindable. Поэтому следует просто let или var. Таким образом, вместо @State private var org : Organization = .shared вы могли бы иметь:

//@State private var org : Organization = .shared
@Bindable var org : Organization = .shared

или:

//@State private var org : Organization = .shared
let org : Organization = .shared

... и это все равно будет работать, поскольку вы используете @Observable.

Вы даже можете использовать @Bindable или let в теле:

struct ContentView: View {

    //@State private var org : Organization = .shared

    var body: some View {
        
        @Bindable var org = Organization.shared // <-- HERE, this will also work
        //let org = Organization.shared // <-- or this (uncomment to use and comment out the @Bindable line above

        List(org.people) {

или даже:

struct ContentView: View {

    var body: some View {
        
        let people = Organization.shared.people // <-- HERE, this will also work

        List(people) {
    

Используете ли вы тот или иной метод, в основном зависит от того, как вы получаете или используете данные. В этом случае вы получаете его непосредственно из класса, но вы можете передать его подпредставлениям в качестве привязки или использовать в теле для развертывания, если имеете дело с опциями.

Пример 1. Инициализируется в корневом представлении как State и передается в подпредставление, которое получает от Binding до State:

struct ContentView: View {
    
    @State private var org : Organization = .shared
    
    var body: some View {
        
        PersonList(organization: $org) // <-- Here, passing State as a binding
        
        Button(action: {
            org.createPeople()
        }) {
            Text("Create People")
        }
    }
}

struct PersonList: View {
    
    @Binding var organization: Organization // <-- Here, receiving binding from parent
    
    var body: some View {
        
        List(organization.people) {
            PersonRow(person: $0)
        }
        .padding()
    }
}

Пример 2. Инициализируется в корневом представлении как State и передается в подпредставление через среду (обратите внимание на отсутствие необходимых параметров).

struct ContentView: View {
    
    @State private var org : Organization = .shared
    
    var body: some View {
        
        PersonList() // <-- Here, notice no parameters
            .environment(org)
        
        Button(action: {
            Organization.shared.createPeople() // <--
        }) {
            Text("Create People")
        }
    }
}

struct PersonList: View {
    
    @Environment(Organization.self) private var org // <-- Here, receiving environment from parent
    
    var body: some View {
        
        List(org.people) {
            PersonRow(person: $0)
        }
        .padding()
    }
}

Однако, если оно передается как среда, а не как состояние, привязки не будет. А если он вам действительно нужен, здесь вам пригодится Bindable. Рассмотрим следующий пример, где я добавил var isActive: Bool = true к классу Организация:

struct ContentView: View {
    
    @State private var org : Organization = .shared
    
    var body: some View {
        
        PersonList() // <-- Here, notice no parameters
            .environment(org)

        
        Button(action: {
            org.createPeople()
        }) {
            Text("Create People")
        }
    }
}

struct PersonList: View {
    
    @Environment(Organization.self) private var org // <-- Here, receiving environment from parent
    
    var body: some View {
        
        @Bindable var organization = org // <-- HERE, get a binding to an observable object (a bindable object)
        
        Toggle(isOn: $organization.isActive, label: { // <-- Here, using Binding format to directly change the status
            Text("Status: \( org.isActive ? "Active" : "Inactive")")
        })
        .fixedSize()

        List(org.people) {
            PersonRow(person: $0)
        }
        .padding()
    }
}

В конце концов, учитывая использование здесь класса, не имеет большого значения, какой из них вы используете, потому что все передается по ссылке. Если вы использовали другие модели, такие как структуры или состояния, которые загружаются из пользовательских настроек по умолчанию и т. д., то понимание различий будет иметь важное значение.

Надеюсь, это поможет.

Спасибо. Это отличная информация. Мне нужно немного с этим поиграть, чтобы по-настоящему все понять. Я привык к AppKit. Это сдвиг в мышлении.

MAH 15.08.2024 02:17

Другие вопросы по теме