navigation-patterns

navigation-patterns

Expert navigation decisions for iOS/tvOS: when NavigationStack vs Coordinator patterns, NavigationPath state management trade-offs, deep link architecture choices, and tab+navigation coordination strategies. Use when designing app navigation, implementing deep links, or debugging navigation state issues. Trigger keywords: NavigationStack, NavigationPath, deep link, routing, tab bar, navigation, programmatic navigation, universal link, URL scheme, navigation state

2星標
0分支
更新於 1/25/2026
SKILL.md
readonlyread-only
name
navigation-patterns
description

"Expert navigation decisions for iOS/tvOS: when NavigationStack vs Coordinator patterns, NavigationPath state management trade-offs, deep link architecture choices, and tab+navigation coordination strategies. Use when designing app navigation, implementing deep links, or debugging navigation state issues. Trigger keywords: NavigationStack, NavigationPath, deep link, routing, tab bar, navigation, programmatic navigation, universal link, URL scheme, navigation state"

version
"3.0.0"

Navigation Patterns — Expert Decisions

Expert decision frameworks for SwiftUI navigation choices. Claude knows NavigationStack syntax — this skill provides judgment calls for architecture decisions and state management trade-offs.


Decision Trees

Navigation Architecture Selection

How complex is your navigation?
├─ Simple (linear flows, 1-3 screens)
│  └─ NavigationStack with inline NavigationLink
│     No Router needed
│
├─ Medium (multiple flows, deep linking required)
│  └─ NavigationStack + Router (ObservableObject)
│     Centralized navigation state
│
└─ Complex (tabs with independent stacks, cross-tab navigation)
   └─ Tab Coordinator + per-tab Routers
      Each tab maintains own NavigationPath

NavigationPath vs Typed Array

Do you need heterogeneous routes?
├─ YES (different types in same stack)
│  └─ NavigationPath (type-erased)
│     path.append(User(...))
│     path.append(Product(...))
│
└─ NO (single route enum)
   └─ @State var path: [Route] = []
      Type-safe, debuggable, serializable

Rule: Prefer typed arrays unless you genuinely need mixed types. NavigationPath's type erasure makes debugging harder.

Deep Link Handling Strategy

When does deep link arrive?
├─ App already running (warm start)
│  └─ Direct navigation via Router
│
└─ App launches from deep link (cold start)
   └─ Is view hierarchy ready?
      ├─ YES → Navigate immediately
      └─ NO → Queue pending deep link
         Handle in root view's .onAppear

Modal vs Push Selection

Is the destination a self-contained flow?
├─ YES (can complete/cancel independently)
│  └─ Modal (.sheet or .fullScreenCover)
│     Examples: Settings, Compose, Login
│
└─ NO (part of current navigation hierarchy)
   └─ Push (NavigationLink or path.append)
      Examples: Detail views, drill-down

NEVER Do

NavigationPath State

NEVER store NavigationPath in ViewModel without careful consideration:

// ❌ ViewModel owns navigation — couples business logic to navigation
@MainActor
final class HomeViewModel: ObservableObject {
    @Published var path = NavigationPath()  // Wrong layer!
}

// ✅ Router/Coordinator owns navigation, ViewModel owns data
@MainActor
final class Router: ObservableObject {
    @Published var path = NavigationPath()
}

@MainActor
final class HomeViewModel: ObservableObject {
    @Published var items: [Item] = []  // Data only
}

NEVER use NavigationPath across tabs:

// ❌ Shared path across tabs — navigation becomes unpredictable
struct MainTabView: View {
    @StateObject var router = Router()  // Single router!

    var body: some View {
        TabView {
            // Both tabs share same path — chaos
        }
    }
}

// ✅ Each tab has independent navigation stack
struct MainTabView: View {
    @StateObject var homeRouter = Router()
    @StateObject var searchRouter = Router()

    var body: some View {
        TabView {
            NavigationStack(path: $homeRouter.path) { ... }
            NavigationStack(path: $searchRouter.path) { ... }
        }
    }
}

NEVER forget to handle deep links arriving before view hierarchy:

// ❌ Race condition — navigation may fail silently
@main
struct MyApp: App {
    @StateObject var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    router.handle(url)  // View may not exist yet!
                }
        }
    }
}

// ✅ Queue deep link for deferred handling
@main
struct MyApp: App {
    @StateObject var router = Router()
    @State private var pendingDeepLink: URL?

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onAppear {
                    if let url = pendingDeepLink {
                        router.handle(url)
                        pendingDeepLink = nil
                    }
                }
                .onOpenURL { url in
                    pendingDeepLink = url
                }
        }
    }
}

Route Design

NEVER use stringly-typed routes:

// ❌ No compile-time safety, typos cause runtime failures
func navigate(to screen: String) {
    switch screen {
    case "profile": ...
    case "setings": ...  // Typo — silent failure
    }
}

// ✅ Enum routes with associated values
enum Route: Hashable {
    case profile(userId: String)
    case settings
}

NEVER put navigation logic in Views:

// ❌ View knows too much about app structure
struct ItemRow: View {
    var body: some View {
        NavigationLink {
            ItemDetailView(item: item)  // View creates destination
        } label: {
            Text(item.name)
        }
    }
}

// ✅ Delegate navigation to Router
struct ItemRow: View {
    @EnvironmentObject var router: Router

    var body: some View {
        Button(item.name) {
            router.navigate(to: .itemDetail(item.id))
        }
    }
}

Navigation State Persistence

NEVER lose navigation state on app termination without consideration:

// ❌ User loses their place when app is killed
@StateObject var router = Router()  // State lost on terminate

// ✅ Persist for important flows (optional based on UX needs)
@SceneStorage("navigationPath") private var pathData: Data?

var body: some View {
    NavigationStack(path: $router.path) { ... }
        .onAppear { router.restore(from: pathData) }
        .onChange(of: router.path) { pathData = router.serialize() }
}

Essential Patterns

Type-Safe Router

@MainActor
final class Router: ObservableObject {
    enum Route: Hashable {
        case userList
        case userDetail(userId: String)
        case settings
        case settingsSection(SettingsSection)
    }

    @Published var path: [Route] = []

    func navigate(to route: Route) {
        path.append(route)
    }

    func pop() {
        guard !path.isEmpty else { return }
        path.removeLast()
    }

    func popToRoot() {
        path.removeAll()
    }

    func replaceStack(with routes: [Route]) {
        path = routes
    }

    @ViewBuilder
    func destination(for route: Route) -> some View {
        switch route {
        case .userList:
            UserListView()
        case .userDetail(let userId):
            UserDetailView(userId: userId)
        case .settings:
            SettingsView()
        case .settingsSection(let section):
            SettingsSectionView(section: section)
        }
    }
}

Deep Link Handler

enum DeepLink {
    case user(id: String)
    case product(id: String)
    case settings

    init?(url: URL) {
        guard let scheme = url.scheme,
              ["myapp", "https"].contains(scheme) else { return nil }

        let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
        let components = path.components(separatedBy: "/")

        switch components.first {
        case "user":
            guard components.count > 1 else { return nil }
            self = .user(id: components[1])
        case "product":
            guard components.count > 1 else { return nil }
            self = .product(id: components[1])
        case "settings":
            self = .settings
        default:
            return nil
        }
    }
}

extension Router {
    func handle(_ deepLink: DeepLink) {
        popToRoot()

        switch deepLink {
        case .user(let id):
            navigate(to: .userList)
            navigate(to: .userDetail(userId: id))
        case .product(let id):
            navigate(to: .productDetail(productId: id))
        case .settings:
            navigate(to: .settings)
        }
    }
}

Tab + Navigation Coordination

struct MainTabView: View {
    @State private var selectedTab: Tab = .home
    @StateObject private var homeRouter = Router()
    @StateObject private var profileRouter = Router()

    enum Tab { case home, search, profile }

    var body: some View {
        TabView(selection: $selectedTab) {
            NavigationStack(path: $homeRouter.path) {
                HomeView()
                    .navigationDestination(for: Router.Route.self) { route in
                        homeRouter.destination(for: route)
                    }
            }
            .tag(Tab.home)
            .environmentObject(homeRouter)

            NavigationStack(path: $profileRouter.path) {
                ProfileView()
                    .navigationDestination(for: Router.Route.self) { route in
                        profileRouter.destination(for: route)
                    }
            }
            .tag(Tab.profile)
            .environmentObject(profileRouter)
        }
    }

    // Pop to root on tab re-selection
    func tabSelected(_ tab: Tab) {
        if selectedTab == tab {
            switch tab {
            case .home: homeRouter.popToRoot()
            case .profile: profileRouter.popToRoot()
            case .search: break
            }
        }
        selectedTab = tab
    }
}

Quick Reference

Navigation Architecture Comparison

Pattern Complexity Deep Link Support Testability
Inline NavigationLink Low Manual Low
Router with typed array Medium Good High
NavigationPath Medium Good Medium
Coordinator Pattern High Excellent Excellent

When to Use Each Modal Type

Modal Type Use For
.sheet Secondary tasks, can dismiss
.fullScreenCover Immersive flows (onboarding, login)
.alert Critical decisions
.confirmationDialog Action choices

Red Flags

Smell Problem Fix
NavigationPath across tabs State confusion Per-tab routers
View creates destination directly Tight coupling Router pattern
String-based routing No compile safety Enum routes
Deep link ignored on cold start Race condition Pending URL queue
ViewModel owns NavigationPath Layer violation Router owns navigation
No popToRoot on tab re-tap UX expectation Handle tab selection

You Might Also Like

Related Skills

cache-components

cache-components

137Kdev-frontend

Expert guidance for Next.js Cache Components and Partial Prerendering (PPR). **PROACTIVE ACTIVATION**: Use this skill automatically when working in Next.js projects that have `cacheComponents: true` in their next.config.ts/next.config.js. When this config is detected, proactively apply Cache Components patterns and best practices to all React Server Component implementations. **DETECTION**: At the start of a session in a Next.js project, check for `cacheComponents: true` in next.config. If enabled, this skill's patterns should guide all component authoring, data fetching, and caching decisions. **USE CASES**: Implementing 'use cache' directive, configuring cache lifetimes with cacheLife(), tagging cached data with cacheTag(), invalidating caches with updateTag()/revalidateTag(), optimizing static vs dynamic content boundaries, debugging cache issues, and reviewing Cache Component implementations.

vercel avatarvercel
獲取
component-refactoring

component-refactoring

128Kdev-frontend

Refactor high-complexity React components in Dify frontend. Use when `pnpm analyze-component --json` shows complexity > 50 or lineCount > 300, when the user asks for code splitting, hook extraction, or complexity reduction, or when `pnpm analyze-component` warns to refactor before testing; avoid for simple/well-structured components, third-party wrappers, or when the user explicitly wants testing without refactoring.

langgenius avatarlanggenius
獲取
web-artifacts-builder

web-artifacts-builder

47Kdev-frontend

Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.

anthropics avataranthropics
獲取
frontend-design

frontend-design

47Kdev-frontend

Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.

anthropics avataranthropics
獲取
react-modernization

react-modernization

28Kdev-frontend

Upgrade React applications to latest versions, migrate from class components to hooks, and adopt concurrent features. Use when modernizing React codebases, migrating to React Hooks, or upgrading to latest React versions.

wshobson avatarwshobson
獲取
tailwind-design-system

tailwind-design-system

28Kdev-frontend

Build scalable design systems with Tailwind CSS v4, design tokens, component libraries, and responsive patterns. Use when creating component libraries, implementing design systems, or standardizing UI patterns.

wshobson avatarwshobson
獲取