Я пытаюсь понять, почему мои представления строк не обновляются, когда я повторно заполняю массив, содержащий их.
В качестве примера у меня есть следующие мнения.
//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
и массив был обновлен? Может кто-нибудь объяснить, что я делаю неправильно?
@loremipsum Спасибо! Я работал над этим несколько дней и почему-то пропустил ни одной ссылки на @Bindable
. Есть ли способ сделать это, сохраняя Equatable
и полагаясь только на идентификатор? У меня есть причина для этой конкретной установки.
Нет, Equatable используется SwiftUI, чтобы узнать, изменилось ли что-то. Если во внимание принимается только идентификатор, то только изменение идентификатора вызовет изменение.
Спасибо! Я бы отметил ваш ответ как правильный, но сделать это в виде комментария невозможно. Не стесняйтесь оставить ответ в любое время.
State
создает глубокую копию Observable
, которую вместо этого следует использовать @Bindable
. Также обратите внимание, что State
всегда должно быть private
.
Эта реализация Equatable
также сообщает SwiftUI обновляться только при изменении id
, вам необходимо включить все свойства для Equatable
.
Этот ответ объясняет, что я делаю неправильно. Последующий вопрос. Есть ли способ сказать самому списку воссоздать все его строки? Я знаю, что нет ничего похожего на reloadData
, но какой-нибудь трюк, чтобы заставить это обновление?
@MAH Вы можете дать списку «идентификатор» и изменить его. Это решение ужасно с точки зрения эффективности: оно все воссоздает.
Понятно. Согласитесь, не лучший подход. Я постараюсь обойти Equatable. Время реструктуризации. Еще раз спасибо за вашу помощь.
@MAH Я только что добавил ответ, который может дать некоторое представление обо всем. Вам не нужно присваивать списку идентификатор, поскольку он автоматически обновляется при изменении данных. Так что, возможно, прочтите это, прежде чем все реструктурировать.
Теперь это просто 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. Это сдвиг в мышлении.
State
создает глубокую копиюObservable
, которую вместо этого следует использовать@Bindable
. Также обратите внимание, чтоState
всегда должно бытьprivate
. Эта реализацияEquatable
также сообщает SwiftUI обновляться только при измененииid
, вам необходимо включить все свойства дляEquatable
.