Представьте, что у вас есть struct
лайк
struct TheObject: Identifiable, Hashable {
var id: String
var title: String
}
Теперь вы храните массив этого типа внутри ViewModel.
class Model: ObservableObject {
@Published var objects = [TheObject(id: "id", title: "title")]
}
Вы создаете NavigationLink
для каждого элемента objects
. Эти навигационные ссылки содержат кнопку, которая заменяет массив objects
тем же объектом (по идентификатору), но с другим свойством.
@StateObject var model = Model()
NavigationStack {
List(model.objects) { object in
NavigationLink("Text", value: object)
}
.navigationDestination(for: TheObject.self) { object in
Text(object.title)
.onTapGesture {
model.objects = [TheObject(id: "id", title: "title changed")]
}
}
}
Чего я ожидал?
Я ожидал, что моя открытая навигационная ссылка будет обновлена новым заголовком.
Что я получил взамен?
Вид не изменился. Вместо этого мне приходится открыть представление и снова открыть его, чтобы увидеть изменения.
Как я могу заставить ожидаемое поведение работать и почему оно в настоящее время ведет себя именно так?
Верно, но это, к сожалению, не имеет отношения к делу.
возможно тебя не хватает @EnvironmentObject var model: Model
Это тоже не то. В этом примере я бы получил ошибку компилятора. Я продолжу и добавлю эти детали, чтобы стало более понятно.
Когда вы используете Text(object.title)
, переданный object
является let
и не является
наблюдается за изменениями в представлении, поэтому оно не изменится при изменении модели
в onTapGesture
.
Но вид изменится, если вы используете модель (которая наблюдается при изменениях), как показано в примере кода.
struct TheObject: Identifiable, Hashable {
var id: String
var title: String
}
class Model: ObservableObject {
@Published var objects = [TheObject(id: "id", title: "title")]
}
struct ContentView: View {
@StateObject private var model = Model()
var body: some View {
NavigationStack {
List(model.objects) { object in
NavigationLink(object.title, value: object) // <--- for testing
}
.navigationDestination(for: TheObject.self) { object in
if let obj = model.objects.first(where: {$0.id == object.id}) {
Text(obj.title) // <--- here
.onTapGesture {
model.objects = [TheObject(id: "id", title: "title changed")]
}
}
// alternative
// if let ndx = model.objects.firstIndex(where: {$0.id == object.id}) {
// Text(model.objects[ndx].title)
// .onTapGesture {
// model.objects[ndx].title = "title changed"
// }
// }
}
}
}
}
РЕДАКТИРОВАТЬ-1:
Альтернативой достижению желаемого является использование Observable
,
более надежная замена ObservableObject class
.
Например:
@Observable class TheObject: Identifiable, Hashable {
static func == (lhs: TheObject, rhs: TheObject) -> Bool {
lhs.id == rhs.id
}
func hash(into hasher: inout Hasher) {
hasher.combine(id)
hasher.combine(title)
}
var id: String
var title: String
init(id: String, title: String) {
self.id = id
self.title = title
}
}
@Observable class Model {
var objects = [TheObject(id: "id", title: "title")]
}
struct ContentView: View {
@State private var model = Model()
var body: some View {
NavigationStack {
List(model.objects) { object in
NavigationLink(object.title, value: object)
}
.navigationDestination(for: TheObject.self) { object in
Text(object.title)
.onTapGesture {
object.title = "title changed"
}
}
}
}
}
Ваше решение отлично работает! Однако я использую Codable Structs с Firebase, поэтому думаю, что @Observable
для меня не может быть и речи. Возможно, это будет полезно другим, кто наткнулся на это. Помимо всего этого, мне интересно, зачем мне вообще нужно использовать этот «обходной путь». Я думал, что соответствие Equatable и Hashable только с помощью id
сработает, но это не так. Не могли бы вы это уточнить?
Извините, но я не понимаю ваших вопросов, ...why I even need to use this "workaround"...
о каком «обходном пути» вы говорите. Аналогично, что вы подразумеваете под ...using id would do the job, which it didn't
.
Под обходным решением я подразумеваю использование оператора first(where: )
вместо прямого доступа к нему. Я идентифицирую TheObject
через и только через удостоверение личности. Я ожидал, что компилятор интерпретирует обновленный TheObject
как «старый», но с измененным свойством (а затем покажет изменение). По крайней мере, это должно произойти, когда я реализую Equatable, чтобы просто проверить равенство свойства ID. По сути, мой вопрос: почему он думает, что это другой объект?
object
в navigationDestination
— это всего лишь копия неизменяемого struct TheObject
. Это let
, его нельзя изменить. Чтобы изменить модель, вам необходимо обратиться к исходной модели в class Model: ObservableObject...
. Потому что это observed
вид, когда вы заявляете @StateObject private var model = Model()
. Чтобы сослаться на этот исходный объект, используйте его id
, чтобы найти его. Это не выход, это нормально. См. Данные мониторинга, чтобы узнать, как управлять данными в вашем приложении.
Спасибо за разъяснения, я ценю это!
Причина, по которой ваше текстовое представление не обновляется, заключается в том, что object
передается в замыкание как копия, а это не тот же экземпляр model
. Чтобы отобразить измененное значение, вам придется передать тот же экземпляр model
в текстовое представление следующим образом:
.navigationDestination(for: TheObject.self) { _ in
Text(model.objects[0].title)
.onTapGesture {
model.objects = [TheObject(id: "id", title: "title changed")]
}
}
Спасибо за ваш ответ. В моем реальном приложении я получаю mode.objects
из своего облака, поэтому мне нужно заменить весь массив. Также внутри массива может быть более одного значения.
похоже ты забыл
List