# NavigationStack
## Intent
Use this pattern for programmatic navigation and deep links, especially when each tab needs an independent navigation history. The key idea is one `NavigationStack` per tab, each with its own path binding and router object.
## Core architecture
- Define a route enum that is `Hashable` and represents all destinations.
- Create a lightweight router (or use a library such as `https://github.com/Dimillian/AppRouter`) that owns the `path` and any sheet state.
- Each tab owns its own router instance and binds `NavigationStack(path:)` to it.
- Inject the router into the environment so child views can navigate programmatically.
- Centralize destination mapping with a single `navigationDestination(for:)` block (or a `withAppRouter()` modifier).
## Example: custom router with per-tab stack
```swift
@MainActor
@Observable
final class RouterPath {
var path: [Route] = []
var presentedSheet: SheetDestination?
func navigate(to route: Route) {
path.append(route)
}
func reset() {
path = []
}
}
enum Route: Hashable {
case account(id: String)
case status(id: String)
}
@MainActor
struct TimelineTab: View {
@State private var routerPath = RouterPath()
var body: some View {
NavigationStack(path: $routerPath.path) {
TimelineView()
.navigationDestination(for: Route.self) { route in
switch route {
case .account(let id): AccountView(id: id)
case .status(let id): StatusView(id: id)
}
}
}
.environment(routerPath)
}
}
```
## Example: centralized destination mapping
Use a shared view modifier to avoid duplicating route switches across screens.
```swift
extension View {
func withAppRouter() -> some View {
navigationDestination(for: Route.self) { route in
switch route {
case .account(let id):
AccountView(id: id)
case .status(let id):
StatusView(id: id)
}
}
}
}
```
Then apply it once per stack:
```swift
NavigationStack(path: $routerPath.path) {
TimelineView()
.withAppRouter()
}
```
## Example: binding per tab (tabs with independent history)
```swift
@MainActor
struct TabsView: View {
@State private var timelineRouter = RouterPath()
@State private var notificationsRouter = RouterPath()
var body: some View {
TabView {
TimelineTab(router: timelineRouter)
NotificationsTab(router: notificationsRouter)
}
}
}
```
## Example: generic tabs with per-tab NavigationStack
Use this when tabs are built from data and each needs its own path without hard-coded names.
```swift
@MainActor
struct TabsView: View {
@State private var selectedTab: AppTab = .timeline
@State private var tabRouter = TabRouter()
var body: some View {
TabView(selection: $selectedTab) {
ForEach(AppTab.allCases) { tab in
NavigationStack(path: tabRouter.binding(for: tab)) {
tab.makeContentView()
}
.environment(tabRouter.router(for: tab))
.tabItem { tab.label }
.tag(tab)
}
}
}
}
```
@MainActor
@Observable
final class TabRouter {
private var routers: [AppTab: RouterPath] = [:]
func router(for tab: AppTab) -> RouterPath {
if let router = routers[tab] { return router }
let router = RouterPath()
routers[tab] = router
return router
}
func binding(for tab: AppTab) -> Binding<[Route]> {
let router = router(for: tab)
return Binding(get: { router.path }, set: { router.path = $0 })
}
}
## Design choices to keep
- One `NavigationStack` per tab to preserve independent history.
- A single source of truth for navigation state (`RouterPath` or library router).
- Use `navigationDestination(for:)` to map routes to views.
- Reset the path when app context changes (account switch, logout, etc.).
- Inject the router into the environment so child views can navigate and present sheets without prop-drilling.
- Keep sheet presentation state on the router if you want a single place to manage modals.
## Pitfalls
- Do not share one path across all tabs unless you want global history.
- Ensure route identifiers are stable and `Hashable`.
- Avoid storing view instances in the path; store lightweight route data instead.
- If using a router object, keep it outside other `@Observable` objects to avoid nested observation.