The iOS 16 Navigation API is data driven: a NavigationLink carries a Hashable value, and navigationDestination(for:) maps the value's type to a destination view. A Route enum turns that into one type-safe switch for a whole feature.
Value-Based Links
Bind each link to a value instead of a destination view. One navigationDestination per value type resolves them.
NavigationStack {
List(products) { product in
NavigationLink(product.title, value: product)
}
.navigationTitle("Products")
.navigationDestination(for: Product.self) { product in
ProductDetailView(product: product)
}
}
The value must conform to Hashable. Stack several navigationDestination modifiers when a screen links to several value types, and use the label-closure initializer of NavigationLink when the link needs custom content.
Placement Rules for navigationDestination
Three rules, and breaking them fails silently:
- Place the modifier inside the
NavigationStack. - Never place it on a child of a lazy container such as
List,ScrollView, orLazyVStack. - When two modifiers handle the same type, the top-level one wins over the lower one.
The Navigator Pattern
Model every route of a feature as one enum, so the whole flow lives in a single switch.
enum Route: Hashable {
case product(Product)
case category(Category)
}
Links produce Route values, and the views stay free of destination knowledge.
NavigationLink(value: Route.category(category)) {
Text(category.query)
}
NavigationLink(value: Route.product(product)) {
Text(product.title)
}
The container view owns the single navigationDestination and switches over the enum.
struct AppContainerView: View {
@StateObject private var store = Store()
var body: some View {
NavigationStack {
MasterView(categories: store.categories, recentProducts: store.products)
.navigationDestination(for: Route.self) { route in
switch route {
case let .category(category):
CategoryView(category: category)
case let .product(product):
ProductDetailView(product: product)
}
}
}
}
}