# NavigationStack

## Contents

- [Intent](#intent)
- [Core architecture](#core-architecture)
- [Example: custom router with per-tab stack](#example-custom-router-with-per-tab-stack)
- [Example: centralized destination mapping](#example-centralized-destination-mapping)
- [Example: binding per tab (tabs with independent history)](#example-binding-per-tab-tabs-with-independent-history)
- [Example: generic tabs with per-tab NavigationStack](#example-generic-tabs-with-per-tab-navigationstack)
- [Design choices to keep](#design-choices-to-keep)
- [Pitfalls](#pitfalls)

## 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 selectedTab: AppTab = .timeline
  @State private var timelineRouter = RouterPath()
  @State private var notificationsRouter = RouterPath()

  var body: some View {
    TabView(selection: $selectedTab) {
      Tab("Timeline", systemImage: "text.bubble", value: .timeline) {
        TimelineTab(router: timelineRouter)
      }
      Tab("Notifications", systemImage: "bell", value: .notifications) {
        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
        Tab(value: tab) {
          NavigationStack(path: tabRouter.binding(for: tab)) {
            tab.makeContentView()
          }
          .environment(tabRouter.router(for: tab))
        } label: {
          tab.label
        }
      }
    }
  }
}
```

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