Add fourteen skills from Dimillian/Skills, integrate the merged Snowflake and WordPress updates into the maintainer sync, and refresh registry metadata, attributions, walkthrough notes, and the 8.9.0 release notes while keeping validation warnings within budget.
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.
|