Taking control of your navigation in SwiftUI with NavigationPath

Taking control of your navigation in SwiftUI with NavigationPath

Understand how to perform data-driven navigation in a SwiftUI app.

SwiftUI's NavigationStack and NavigationPath provide a powerful and flexible way to perform programmatic navigation in your app. When managing navigation in a SwiftUI app, you often want to push and pop views programmatically, and NavigationPath enables this while also maintaining strong type-safety and flexibility.

The NavigationStack(root:) default initializer sets the root of your navigation hierarchy and handles the navigation path behind the scenes. If you want more control over the navigation path and enable a programmatic approach to your navigation, you can store it with a @State variable and pass it to the NavigationStack(path:root:) initializer. The path parameter needs to be of Binding<Data> type and there are two ways you can use it.

Type-Specific Navigation

The first method uses an array of a specific type that conforms to the Hashable protocol. This is useful when a single data type drives all the views in your navigation stack.

@State private var path: [Color] = []

NavigationStack(path: $path) {
    List {
        ForEach(colors, id: \.self) { color in
            Button {
                path.append(color)
            } label: {
                ...
            }
        }
    }
    .navigationDestination(for: Color.self) { color in
        VStack {
            color
            ...
            Button("Pop to root") {
                path.removeAll()
            }
        }
        ...
    }
}

In the example above, the navigation stack is backed by an array of Color objects that acts as the NavigationPath. Whenever an item is pushed into the path array, the navigationDestination(for:) modifier handles the presentation of the corresponding destination whenever an instance of the Color type is appended to the path. The path.removeAll() is used to empty the array, cleaning the navigation stack and going back to the root view.

This approach is ideal for clean, type-safe navigation with minimal setup, particularly when dealing with a single type of data, as it unlocks the possibility of managing the navigation path as an array.

0:00
/0:09

The array is empty when in the root view, while it gets filled as you navigate deeper into the navigation stack, with the last element of the array being the currently displayed view. This means that if we want to navigate to a new view, we append a new element to the array, while if we're going to navigate back, we remove the last element from the array.

Generic NavigationPath for Multiple Types

When your navigation stack may contain multiple types (e.g., Color, String, or even custom types), NavigationPath is the better option. It behaves as a type-erased list of data, yet retains enough type information for SwiftUI to render the appropriate view when an item is pushed.

@State private var path = NavigationPath()

NavigationStack(path: $path) {
    List {
        Section("Colors") {
            ForEach(colors, id: \.self) { color in
                Button {
                    path.append(color)
                } label: {
                    ...
                }
            }
        }
        Section("Genres") {
            ForEach(genres, id: \.self) { genre in
                Button {
                    path.append(genre)
                } label: {
                    ...
                }
            }
        }
    }
    .navigationDestination(for: Color.self) { color in
        VStack {
            ...
            Button("Pop to root") {
                path.removeLast(path.count)
            }
        }
        ...
    }
    .navigationDestination(for: String.self) { genre in
        VStack {
            ...
            Button("Pop to root") {
                path.removeLast(path.count)
            }
        }
        ...
    }
}

With NavigationPath you can append different data types to the stack. Make sure each type has a separate navigationDestination(for:destination:) modifier associated with that type to describe the UI of the corresponding destination view.

If you append a value to the NavigationPath but a navigation destination modifier does not handle the type, you will get no errors at compile time and the NavigationStack will show the user an empty view with a warning symbol.

This approach is more flexible, especially for apps whose navigation depends on different data models.