Programmatic navigation with navigation destination in SwiftUI

Programmatic navigation with navigation destination in SwiftUI

Learn how to use the navigation destination modifier for triggering navigation in a SwiftUI app.

SwiftUI’s modern NavigationStack isolates the logic of how navigation is triggered from how the UI handles it. While NavigationLink(destination:label:) can directly navigate to a view, the navigation destination modifiers provide a more flexible, data-driven approach. Let's explore the different variations of the modifier and when to use each of them.

Use navigationDestination(isPresented:destination:) when a simple boolean value condition triggers navigation. This modifier associates a destination view with a binding that can be used to push the view onto a NavigationStack.

struct ContentView: View {

    // 1.
    @State private var isAddingProduct: Bool = false

    var body: some View {
    
        NavigationStack(path: $path) {
            List {
                // Views for the list here
            }
            
            // 3.
            .navigationDestination(isPresented: $isAddingProduct) {
                // 4.
                AddProductView()
            }
            
            .toolbar {
                ToolbarItem {
                    Button(action: {
                        // 2.
                        isAddingProduct = true
                    }, label: {
                        Label("Add", systemImage: "plus")
                    })
                }
            }
        }
    }
}
  1. A @State Boolean variable
  2. A button that will set the variable to true
  3. Bind this variable in the `isPresented` parameter of the modifier
  4. The destination view in the closure of the modifier

A common use case is tapping a button that toggles a view to appear without passing custom data.

The navigationDestination(item:destination:) modifier is ideal when triggering the navigation relies on selecting a value. SwiftUI will push the destination view to appear whenever the optional binding value becomes non-nil.

struct ContentView: View {
    
    // 1.
    @State private var selectedFood: OtherFood? = nil
    
    @State private var otherFoods: [OtherFood] = [
        OtherFood(name: "Pizza", symbol: "🍕")
    ]
    
    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("Fruit") {
                    ...
                }
                Section("Vegetables") {
                    ...
                }
                
                Section("Other Foods") {
                    ForEach(otherFoods, id: \.self) { food in
                        Button(action: {
                            // 2.
                            selectedFood = food 
                        }, label: {
                            HStack {
                                Text(food.symbol)
                                Text(food.name)
                            }
                        })
                    }
                }
            }
            // 3.
            .navigationDestination(item: $selectedFood) { food in 
                // 4.
                OtherFoodDetailView(food: food) 
            }
        }
    }
}
  1. A @State optional value of any Type. Mind that conformance of the Type to Hashable protocol is needed.
  2. A button to select the value
  3. Bind the optional value in the `item` parameter of the modifier
  4. The closure of the modifier returns the value, and it is here that you define the destination view.

Since the item is bound, you can navigate to views that can change the value somehow, like for editing or selection purposes.

If you are trying to develop more complex or dynamic flows, use .navigationDestination(for:destination:) for one or more Hashable types. This approach allows SwiftUI to match pushed values in a NavigationPath to their respective destinations.

struct ContentView: View {
    // 1.
    @State private var path = NavigationPath()

    @State private var fruits = [
        Fruit(name: "Apple", symbol: "🍎"),
        Fruit(name: "Banana", symbol: "🍌"),
        Fruit(name: "Cherry", symbol: "🍒")
    ]
    
    @State private var vegetables: [Vegetable] = [
        Vegetable(name: "Carrot", symbol: "🥕"),
        Vegetable(name: "Broccoli", symbol: "🥦")
    ]

    var body: some View {
        NavigationStack(path: $path) {
            List {
                Section("Fruit") {
                    ForEach(fruits, id: \.self) { fruit in
                        // 2.
                        NavigationLink(value: fruit) {
                            HStack {
                                Text(fruit.symbol)
                                Text(fruit.name)
                            }
                        }
                    }
                }
                
                Section("Vegetables") {
                    ForEach(vegetables, id: \.self) { vegetable in
                        Button(action: {
                            // 2.
                            path.append(vegetable)
                        }, label: {
                            HStack {
                                Text(vegetable.symbol)
                                Text(vegetable.name)
                            }
                        })
                    }
                }
            }

            // 3.
            .navigationDestination(for: Fruit.self) { fruit in
                FruitDetailView(fruit: fruit)
            }
            
            .navigationDestination(for: Vegetable.self) { vegetable in
                VegetableDetailView(vegetable: vegetable)
            }

        }
    }
}
  1. (Optional) A NavigationPath
    1. If you don't use it explicitly, SwiftUI will handle it under the hood for you
    2. You might need it if you want to have full control over navigation
  2. A value pushed to the NavigationPath, which can either be:
    1. The value parameter of a NavigationLink, or
    2. Appending the value to the NavigationPath programmatically
  3. Ensure the type you're using conforms to the Hashable protocol and handle every type with the modifier

This is best used when you need to enable automatic navigation based on the type of data when calling path.append(value) from a button on a NavigationPath or when you push a value with a NavigationLink(value:label:).

This is commonly used to select an item from a list and navigate to its detail view.