Imagine you have a struct
like
struct TheObject: Identifiable, Hashable {
var id: String
var title: String
}
Now you store an array of that type inside a ViewModel
class Model: ObservableObject {
@Published var objects = [TheObject(id: "id", title: "title")]
}
You create a NavigationLink
for each item of objects
. Those NavigationLinks feature a button, which replaces the objects
array with the same object (by id) but a different property
@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")]
}
}
}
What did I expect to happen?
I expected my opened NavigationLink to be updated with the new title.
What did I get instead?
The view did not change. Instead I have to pop the view and reopen it to reveal the changes.
How can I get my expected behavior to work and why does it currently behave like that?
When you use Text(object.title)
the passed in object
is a let
and is not
observed for changes by the view, so it will not change when you change the model
in the onTapGesture
.
But the view will change if you use the model (that is observed for changes), as shown in the example code.
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"
// }
// }
}
}
}
}
EDIT-1:
An alternative to achieve what you want, is to use Observable
,
the more robust replacement of ObservableObject class
.
For example:
@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"
}
}
}
}
}