# NavigationStack and Routers

Part of the `swiftui-navigation` skill. Use this when navigation is driven from
code or from deep links and every tab needs a history of its own: one
`NavigationStack` per tab, each bound to its own path and router.

## Building blocks

- A `Hashable` route enum that names every screen you can push.
- A small router object holding both the navigation path and any sheet state. A packaged
  router such as AppRouter (https://github.com/Dimillian/AppRouter) works the
  same way.
- One router per tab, with `NavigationStack(path:)` bound to that router.
- The router placed in the environment, so any child can navigate.
- Destinations mapped once: one `navigationDestination(for:)` switch, or the
  same switch wrapped in a `withAppRouter()` modifier.

## A router with a per-tab stack

```swift
import SwiftUI
import Observation

enum Route: Hashable {
    case trail(id: Trail.ID)
    case ranger(id: Ranger.ID)
    case photos(trailID: Trail.ID)
}

@MainActor @Observable
final class Router {
    var presentedSheet: SheetDestination?
    var path: [Route] = []

    func popToRoot() { path.removeAll() }
    func push(_ next: Route) { path.append(next) }
}

struct ExploreTab: View {
    @State private var nav = Router()

    var body: some View {
        NavigationStack(path: $nav.path) {
            ExploreHome()
                .navigationDestination(for: Route.self) { target in
                    switch target {
                    case .trail(let id): TrailDetailView(trailID: id)
                    case .ranger(let id): RangerView(rangerID: id)
                    case .photos(let id): PhotoGridView(trailID: id)
                    }
                }
        }
        .environment(nav)
    }
}
```

The path here is a plain `[Route]` array. That is often simpler than a
`NavigationPath` when every destination is the same enum.

## Mapping destinations once

Put the switch in a modifier and apply it at the root of each stack, so no
screen repeats it.

```swift
extension View {
    func withAppRouter() -> some View {
        navigationDestination(for: Route.self) { target in
            switch target {
            case .trail(let id): TrailDetailView(trailID: id)
            case .ranger(let id): RangerView(rangerID: id)
            case .photos(let id): PhotoGridView(trailID: id)
            }
        }
    }
}
```

## One router per tab

The tab container keeps a separate router in `@State` for each tab and hands
each one to its tab.

```swift
enum LibraryTab: Hashable { case explore, saved }

struct MainTabs: View {
    @State private var exploreRouter = Router()
    @State private var savedRouter = Router()
    @State private var tab: LibraryTab = .explore

    var body: some View {
        TabView(selection: $tab) {
            Tab("Explore", systemImage: "map", value: LibraryTab.explore) {
                ExploreStack(router: exploreRouter)
            }
            Tab("Saved", systemImage: "bookmark", value: LibraryTab.saved) {
                SavedStack(router: savedRouter)
            }
        }
    }
}

struct ExploreStack: View {
    @Bindable var router: Router

    var body: some View {
        NavigationStack(path: $router.path) { ExploreHome().withAppRouter() }
            .environment(router)
    }
}
```

## Data-driven tabs, each with its own stack

When the tab list comes from data, a `TabRouter` creates a `Router` for a tab
the first time it is asked, caches it, and builds path bindings on demand.

```swift
@MainActor @Observable
final class TabRouter {
    private var cache: [AppTab: Router] = [:]

    func router(for key: AppTab) -> Router {
        if let known = cache[key] { return known }
        let made = Router()
        cache[key] = made
        return made
    }

    func binding(for key: AppTab) -> Binding<[Route]> {
        let owner = router(for: key)
        return Binding(get: { owner.path }, set: { owner.path = $0 })
    }
}

struct DynamicTabs: View {
    @State private var selected: AppTab = .home
    @State private var routers = TabRouter()

    var body: some View {
        TabView(selection: $selected) {
            ForEach(AppTab.allCases) { item in
                Tab(value: item) {
                    NavigationStack(path: routers.binding(for: item)) {
                        item.rootView.withAppRouter()
                    }
                    .environment(routers.router(for: item))
                } label: {
                    item.label
                }
            }
        }
    }
}
```

## Rules worth keeping

- One stack per tab, so each tab remembers where the user was.
- One owner of navigation state: your router or the packaged one, never both.
- Push through route values mapped by `navigationDestination(for:)`.
- Clear the path when the app context changes, such as switching accounts or
  signing out.
- Put the router in the environment so navigation and sheets need no bindings
  passed through intermediate views.
- Sheet state may also live on the router, so modals have one home.

## Pitfalls

- A path shared across tabs, unless you really want one global history.
- Route identifiers that are not stable or not `Hashable`.
- View instances stored in the path instead of route data.
- A router passed as a plain `let` or read from `@Environment` and then used as
  `$router.path`: no binding exists without `@Bindable`, so the stack view needs
  `@Bindable var router: Router` (as `ExploreStack` has) or a local
  `@Bindable var router = router` in `body`.
