A SwiftUI project demonstrating a safer, more composable way to keep detail views in sync with list data using NavigationStack
.
When using .navigationDestination(for:)
with a struct, the view receives a copy of the item at the time of navigation. If the backing data changes later (e.g., after editing), the destination does not reflect those changes.
This can lead to stale data in views like:
.navigationDestination(for: Contact.self) { contact in
ContactDetailView(contact: contact)
}
- A user taps a contact in a list and navigates to a detail view.
- They edit the contact via a sheet and save changes.
- The sheet dismisses, but the detail view still shows the old contact info.
Apple addresses this in FoodTruck, using a binding method in the view model:
public func orderBinding(for id: Order.ID) -> Binding<Order> {
Binding<Order> {
guard let index = self.orders.firstIndex(where: { $0.id == id }) else {
fatalError()
}
return self.orders[index]
} set: { newValue in
guard let index = self.orders.firstIndex(where: { $0.id == id }) else {
fatalError()
}
self.orders[index] = newValue
}
}
This avoids stale data by binding directly to the data source, but it introduces runtime risk via fatalError()
.
This demo introduces a reusable, safer approach:
struct BindedNavigationDestination<Item: Identifiable & Hashable, Destination: View>: ViewModifier {
@Binding var itemList: [Item]
v@ViewBuilder var destination: (Binding<Item>) -> Destination
func body(content: Content) -> some View {
content
.navigationDestination(for: Item.self) { item in
if let index = itemList.firstIndex(where: { $0.id == item.id }) {
destination($itemList[index])
}
}
}
}
- No
fatalError
- Safer and more declarative
- Easily reused across projects
Three tabs showing:
- The stale behavior (
.navigationDestination
) - Example of Apple's workaround (
contactBinding(for:)
) - This modifier’s fix (
bindedNavigationDestination
)
This modifier is part of NnSwiftUIKit, a collection of custom SwiftUI view modifiers and utilities.