navigation-patterns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

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.

SwiftUI导航选择的专家决策框架。Claude精通NavigationStack语法——本技能为架构决策和状态管理权衡提供专业判断。

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
你的导航复杂度如何?
├─ 简单(线性流程,1-3个页面)
│  └─ 搭配内嵌NavigationLink的NavigationStack
│     无需Router
├─ 中等(多流程,需深度链接)
│  └─ NavigationStack + Router(ObservableObject)
│     集中式导航状态
└─ 复杂(带独立栈的标签页,跨标签导航)
   └─ Tab Coordinator + 每个标签页独立Router
      每个标签页维护自身的NavigationPath

NavigationPath vs Typed Array

NavigationPath vs 类型化数组

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.
你需要异构路由吗?
├─ 是(同一栈中存在不同类型)
│  └─ NavigationPath(类型擦除)
│     path.append(User(...))
│     path.append(Product(...))
└─ 否(单一路由枚举)
   └─ @State var path: [Route] = []
      类型安全、可调试、可序列化
规则:除非确实需要混合类型,否则优先选择类型化数组。NavigationPath的类型擦除会增加调试难度。

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
深度链接何时到达?
├─ 应用已运行(热启动)
│  └─ 通过Router直接导航
└─ 应用从深度链接启动(冷启动)
   └─ 视图层级是否就绪?
      ├─ 是 → 立即导航
      └─ 否 → 队列化待处理深度链接
         在根视图的.onAppear中处理

Modal vs Push Selection

模态弹窗 vs 推送页面选型

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

目标页面是独立完整的流程吗?
├─ 是(可独立完成/取消)
│  └─ 模态弹窗(.sheet或.fullScreenCover)
│     示例:设置、内容编辑、登录
└─ 否(属于当前导航层级的一部分)
   └─ 推送页面(NavigationLink或path.append)
      示例:详情页、层级钻取

NEVER Do

绝对禁忌

NavigationPath State

NavigationPath状态管理

NEVER store NavigationPath in ViewModel without careful consideration:
swift
// ❌ 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:
swift
// ❌ 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:
swift
// ❌ 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
                }
        }
    }
}
绝对不要在未充分考虑的情况下将NavigationPath存储在ViewModel中:
swift
// ❌ ViewModel拥有导航逻辑——业务逻辑与导航耦合
@MainActor
final class HomeViewModel: ObservableObject {
    @Published var path = NavigationPath()  // 错误的层级!
}

// ✅ Router/Coordinator拥有导航逻辑,ViewModel仅管理数据
@MainActor
final class Router: ObservableObject {
    @Published var path = NavigationPath()
}

@MainActor
final class HomeViewModel: ObservableObject {
    @Published var items: [Item] = []  // 仅管理数据
}
绝对不要跨标签页共享NavigationPath:
swift
// ❌ 跨标签页共享路径——导航变得不可预测
struct MainTabView: View {
    @StateObject var router = Router()  // 单一Router!

    var body: some View {
        TabView {
            // 两个标签页共享同一路径——混乱不堪
        }
    }
}

// ✅ 每个标签页拥有独立的导航栈
struct MainTabView: View {
    @StateObject var homeRouter = Router()
    @StateObject var searchRouter = Router()

    var body: some View {
        TabView {
            NavigationStack(path: $homeRouter.path) { ... }
            NavigationStack(path: $searchRouter.path) { ... }
        }
    }
}
绝对不要忘记处理视图层级就绪前到达的深度链接:
swift
// ❌ 竞态条件——导航可能静默失败
@main
struct MyApp: App {
    @StateObject var router = Router()

    var body: some Scene {
        WindowGroup {
            ContentView()
                .onOpenURL { url in
                    router.handle(url)  // 视图可能尚未存在!
                }
        }
    }
}

// ✅ 队列化深度链接以延迟处理
@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:
swift
// ❌ 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:
swift
// ❌ 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))
        }
    }
}
绝对不要使用字符串类型的路由:
swift
// ❌ 无编译时安全保障,拼写错误导致运行时失败
func navigate(to screen: String) {
    switch screen {
    case "profile": ...
    case "setings": ...  // 拼写错误——静默失败
    }
}

// ✅ 带关联值的枚举路由
enum Route: Hashable {
    case profile(userId: String)
    case settings
}
绝对不要将导航逻辑放在视图中:
swift
// ❌ 视图对应用结构了解过多
struct ItemRow: View {
    var body: some View {
        NavigationLink {
            ItemDetailView(item: item)  // 视图创建目标页面
        } label: {
            Text(item.name)
        }
    }
}

// ✅ 将导航委托给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:
swift
// ❌ 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() }
}

绝对不要在未考虑用户体验的情况下,应用终止时丢失导航状态:
swift
// ❌ 应用终止时用户丢失当前位置
@StateObject var router = Router()  // 终止时状态丢失

// ✅ 为重要流程持久化状态(根据UX需求可选)
@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

类型安全Router

swift
@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)
        }
    }
}
swift
@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

深度链接处理器

swift
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)
        }
    }
}
swift
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

标签栏+导航协调器

swift
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
    }
}

swift
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)
        }
    }

    // 重新选中标签栏时返回根页面
    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

导航架构对比

PatternComplexityDeep Link SupportTestability
Inline NavigationLinkLowManualLow
Router with typed arrayMediumGoodHigh
NavigationPathMediumGoodMedium
Coordinator PatternHighExcellentExcellent
模式复杂度深度链接支持可测试性
内嵌NavigationLink手动实现
搭配类型化数组的Router良好
NavigationPath良好
Coordinator模式优秀优秀

When to Use Each Modal Type

各模态弹窗类型适用场景

Modal TypeUse For
.sheet
Secondary tasks, can dismiss
.fullScreenCover
Immersive flows (onboarding, login)
.alert
Critical decisions
.confirmationDialog
Action choices
模态类型适用场景
.sheet
次要任务,可关闭
.fullScreenCover
沉浸式流程(引导页、登录)
.alert
关键决策
.confirmationDialog
操作选择

Red Flags

警示信号

SmellProblemFix
NavigationPath across tabsState confusionPer-tab routers
View creates destination directlyTight couplingRouter pattern
String-based routingNo compile safetyEnum routes
Deep link ignored on cold startRace conditionPending URL queue
ViewModel owns NavigationPathLayer violationRouter owns navigation
No popToRoot on tab re-tapUX expectationHandle tab selection
问题迹象核心问题修复方案
跨标签页共享NavigationPath状态混乱每个标签页独立Router
视图直接创建目标页面紧耦合Router模式
字符串类型路由无编译安全保障枚举路由
冷启动时深度链接被忽略竞态条件待处理URL队列
ViewModel拥有NavigationPath层级违规Router管理导航
重新点击标签栏不返回根页面不符合UX预期处理标签栏选择事件