I'm running into an issue with .navigationDestination
. When the user clicks the Calculate button, a new view opens with a list of the items. Then the user can click on a row and get more details. However, it shows a new list with items instead, with the following error in the console:
A navigationDestination for “testApp.Item” was declared earlier on the stack. Only the destination declared closest to the root view of the stack will be used.
In fact, if you look closely, the details show briefly and then the list is shown again.
I have been trying to find a solution for a while now, and there are many similar questions, and the answer is always .navigationDestination
should be as high in the hierarchy as possible, which I think is the case in my code.
So something else must be going on, but I am lost.
What am I missing here?
Here is a MRE:
import SwiftUI
struct Calculator {
let items = [
Item(name: "One", number: 1),
Item(name: "Two", number: 2),
Item(name: "Three", number: 3)
]
}
struct Item: Hashable {
var name: String
var number: Int
}
struct ContentView: View {
@State private var didCalculate = false
@State private var path = NavigationPath()
let calculator = Calculator()
var body: some View {
NavigationStack(path: $path) {
Button("Calculate") {
// in real app a long calculation here inside a Task
didCalculate = true
}
.navigationDestination(isPresented: $didCalculate) {
ItemsView(items: calculator.items)
}
}
}
}
struct ItemsView: View {
var items: [Item]
var body: some View {
List(items, id: \.self) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
ItemDetailView(item: item)
}
}
}
struct ItemRow: View {
var item: Item
var body: some View {
Text(item.name)
}
}
struct ItemDetailView: View {
var item: Item
var body: some View {
VStack {
Text(item.name)
Text(String(item.number))
}
}
}
From my experience, .navigationDestination(isPresented:destination:)
doesn't mix well with value-based navigation. If the isPresented
binding is till true when you navigate to another view with a value, the destination will be pushed onto the stack again. This is possibly why SwiftUI is saying the destination for Item.self
is already declared earlier in the stack. "Earlier" is when ItemsView
first gets added to the stack, and ItemsView
is now getting added to the stack again, redeclaring the same navigation destination for Item.self
.
The destinations added this way also doesn't affect the navigation path, making it more annoying to use with value-based navigation.
I would also use value-based navigation to navigate to ItemsView
. You can declare a new dummy struct/enum to use as the destination's type. If you have multiple destinations, an enum would be more convenient.
struct ItemsViewDestination: Hashable {}
var body: some View {
NavigationStack(path: $path) {
Button("Calculate") {
// navigate like this:
path.append(ItemsViewDestination())
}
.navigationDestination(for: ItemsViewDestination.self) { _ in
ItemsView(items: calculator.items)
}
}
}