Search code examples
iosswiftuiswiftui-navigationstack

navigationDestination was declared earlier on the stack


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))
        }
    }
}

Solution

  • 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)
            }
        }
    }