
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.
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.