160 lines
4.2 KiB
Markdown
160 lines
4.2 KiB
Markdown
# 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.
|