axiom-swiftui-nav

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Navigation

SwiftUI 导航

When to Use This Skill

何时使用此技能

Use when:
  • Choosing navigation architecture (NavigationStack vs NavigationSplitView vs TabView)
  • Implementing programmatic navigation with NavigationPath
  • Setting up deep linking and URL routing
  • Implementing state restoration for navigation
  • Adopting Tab/Sidebar patterns (iOS 18+)
  • Implementing coordinator/router patterns
  • Requesting code review of navigation implementation before shipping
适用于以下场景:
  • 选择导航架构(NavigationStack vs NavigationSplitView vs TabView)
  • 使用NavigationPath实现程序化导航
  • 设置深度链接和URL路由
  • 实现导航的状态恢复
  • 采用标签栏/侧边栏模式(iOS 18+)
  • 实现协调器/路由器模式
  • 上线前请求对导航实现进行代码评审

Related Skills

相关技能

  • Use
    axiom-swiftui-nav-diag
    for systematic troubleshooting of navigation failures
  • Use
    axiom-swiftui-nav-ref
    for comprehensive API reference (including Tab customization, iOS 26+ features) with all WWDC examples
  • 使用
    axiom-swiftui-nav-diag
    对导航失败问题进行系统性排查
  • 使用
    axiom-swiftui-nav-ref
    获取完整的API参考(包括标签栏自定义、iOS 26+功能)以及所有WWDC示例

Example Prompts

示例提示

These are real questions developers ask that this skill is designed to answer:
以下是开发者实际会提出的、本技能可解答的问题:

1. "Should I use NavigationStack or NavigationSplitView for my app?"

1. "我应该为我的应用使用NavigationStack还是NavigationSplitView?"

-> The skill provides a decision tree based on device targets, content hierarchy depth, and multiplatform requirements
-> 本技能会根据设备目标、内容层级深度和多平台要求提供决策树

2. "How do I navigate programmatically in SwiftUI?"

2. "如何在SwiftUI中实现程序化导航?"

-> The skill shows NavigationPath manipulation patterns for push, pop, pop-to-root, and deep linking
-> 本技能展示用于推送、弹出、返回根视图和深度链接的NavigationPath操作模式

3. "My deep links aren't working. The app opens but shows the wrong screen."

3. "我的深度链接无法正常工作。应用能打开,但显示错误的页面。"

-> The skill covers URL parsing patterns, path construction order, and timing issues with onOpenURL
-> 本技能涵盖URL解析模式、路径构建顺序以及onOpenURL的时序问题

4. "Navigation state is lost when my app goes to background."

4. "应用进入后台后导航状态丢失。"

-> The skill demonstrates Codable NavigationPath, SceneStorage persistence, and crash-resistant restoration
-> 本技能演示可编码的NavigationPath、SceneStorage持久化以及防崩溃的恢复方案

5. "How do I implement a coordinator pattern in SwiftUI?"

5. "如何在SwiftUI中实现协调器模式?"

-> The skill provides Router pattern examples alongside guidance on when coordinators add value vs complexity

-> 本技能提供路由器模式示例,同时指导何时使用协调器能带来价值,何时会增加复杂度

Red Flags — Anti-Patterns to Prevent

警示 — 需避免的反模式

If you're doing ANY of these, STOP and use the patterns in this skill:
如果你正在做以下任何一件事,请立即停止并使用本技能中的模式:

❌ CRITICAL — Never Do These

❌ 严重错误 — 绝对不要这样做

1. Using deprecated NavigationView on iOS 16+

1. 在iOS 16+中使用已弃用的NavigationView

swift
// ❌ WRONG — Deprecated, different behavior on iOS 16+
NavigationView {
    List { ... }
}
.navigationViewStyle(.stack)
Why this fails NavigationView is deprecated since iOS 16. It lacks NavigationPath support, making programmatic navigation and deep linking unreliable. Different behavior across iOS versions causes bugs.
swift
// ❌ 错误 — 已弃用,在iOS 16+上行为不同
NavigationView {
    List { ... }
}
.navigationViewStyle(.stack)
失败原因 NavigationView自iOS 16起已被弃用。它不支持NavigationPath,导致程序化导航和深度链接不可靠。不同iOS版本间的行为差异会引发Bug。

2. Using view-based NavigationLink for programmatic navigation

2. 使用基于视图的NavigationLink实现程序化导航

swift
// ❌ WRONG — Cannot programmatically control
NavigationLink("Recipe") {
    RecipeDetail(recipe: recipe)  // View destination, no value
}
Why this fails View-based links cannot be controlled programmatically. No way to deep link or pop to this destination. Deprecated since iOS 16.
swift
// ❌ 错误 — 无法进行程序化控制
NavigationLink("Recipe") {
    RecipeDetail(recipe: recipe)  // 视图目标,无值绑定
}
失败原因 基于视图的链接无法被程序化控制。无法实现深度链接或返回到该目标页面。自iOS 16起已被弃用。

3. Putting navigationDestination inside lazy containers

3. 在惰性容器内放置navigationDestination

swift
// ❌ WRONG — May not be loaded when needed
LazyVGrid(columns: columns) {
    ForEach(items) { item in
        NavigationLink(value: item) { ... }
            .navigationDestination(for: Item.self) { item in  // Don't do this
                ItemDetail(item: item)
            }
    }
}
Why this fails Lazy containers don't load all views immediately. navigationDestination may not be visible to NavigationStack, causing navigation to silently fail.
swift
// ❌ 错误 — 可能在需要时未加载
LazyVGrid(columns: columns) {
    ForEach(items) { item in
        NavigationLink(value: item) { ... }
            .navigationDestination(for: Item.self) { item in  // 不要这样做
                ItemDetail(item: item)
            }
    }
}
失败原因 惰性容器不会立即加载所有视图。navigationDestination可能无法被NavigationStack识别,导致导航静默失败。

4. Storing full model objects in NavigationPath for restoration

4. 在NavigationPath中存储完整模型对象用于状态恢复

swift
// ❌ WRONG — Duplicates data, stale on restore
class NavigationModel: Codable {
    var path: [Recipe] = []  // Full Recipe objects
}
Why this fails Duplicates data already in your model. On restore, Recipe data may be stale (edited/deleted elsewhere). Use IDs and resolve to current data.
swift
// ❌ 错误 — 数据重复,恢复时可能过期
class NavigationModel: Codable {
    var path: [Recipe] = []  // 完整的Recipe对象
}
失败原因 会重复已存在于模型中的数据。恢复时,Recipe数据可能已过期(在其他地方被编辑/删除)。应使用ID并在恢复时解析为当前数据。

5. Modifying NavigationPath outside MainActor

5. 在MainActor外部修改NavigationPath

swift
// ❌ WRONG — UI update off main thread
Task.detached {
    await viewModel.path.append(recipe)  // Background thread
}
Why this fails NavigationPath binds to UI. Modifications must happen on MainActor or navigation state becomes corrupted. Can cause crashes or silent failures.
swift
// ❌ 错误 — 在后台线程更新UI
Task.detached {
    await viewModel.path.append(recipe)  // 后台线程
}
失败原因 NavigationPath与UI绑定。修改操作必须在MainActor中执行,否则导航状态会损坏。可能导致崩溃或静默失败。

6. Missing @MainActor isolation for navigation state

6. 导航状态未使用@MainActor隔离

swift
// ❌ WRONG — Not MainActor isolated
class Router: ObservableObject {
    @Published var path = NavigationPath()  // No @MainActor
}
Why this fails In Swift 6 strict concurrency, @Published properties accessed from SwiftUI views require MainActor isolation. Causes data race warnings and potential crashes.
swift
// ❌ 错误 — 未使用MainActor隔离
class Router: ObservableObject {
    @Published var path = NavigationPath()  // 无@MainActor
}
失败原因 在Swift 6严格并发模式下,SwiftUI视图访问的@Published属性需要MainActor隔离。会导致数据竞争警告和潜在崩溃。

7. Not handling navigation state in multi-tab apps

7. 未在多标签应用中处理导航状态

swift
// ❌ WRONG — Shared NavigationPath across tabs
TabView {
    Tab("Home") { HomeView() }
    Tab("Settings") { SettingsView() }
}
// All tabs share same NavigationStack — wrong!
Why this fails Each tab should have its own NavigationStack to preserve navigation state when switching tabs. Shared state causes confusing UX.
swift
// ❌ 错误 — 所有标签共享同一个NavigationPath
TabView {
    Tab("Home") { HomeView() }
    Tab("Settings") { SettingsView() }
}
// 所有标签共享同一个NavigationStack — 错误!
失败原因 每个标签应拥有自己的NavigationStack以在切换标签时保留导航状态。共享状态会导致混乱的用户体验。

8. Ignoring NavigationPath decoding errors

8. 忽略NavigationPath解码错误

swift
// ❌ WRONG — Crashes on invalid data
let path = NavigationPath(try! decoder.decode(NavigationPath.CodableRepresentation.self, from: data))
Why this fails User may have deleted items that were in the path. Schema may have changed. Force unwrap causes crash on restore.

swift
// ❌ 错误 — 无效数据会导致崩溃
let path = NavigationPath(try! decoder.decode(NavigationPath.CodableRepresentation.self, from: data))
失败原因 用户可能已删除路径中包含的项目。架构可能已更改。强制解包会导致恢复时崩溃。

Mandatory First Steps

强制前置步骤

ALWAYS complete these steps before implementing navigation:
swift
// Step 1: Identify your navigation structure
// Ask: Single stack? Multi-column? Tab-based with per-tab navigation?
// Record answer before writing any code

// Step 2: Choose container based on structure
// Single stack (iPhone-primary): NavigationStack
// Multi-column (iPad/Mac-primary): NavigationSplitView
// Tab-based: TabView with NavigationStack per tab

// Step 3: Define your value types for navigation
// All values pushed on NavigationStack must be Hashable
// For deep linking/restoration, also Codable
struct Recipe: Hashable, Codable, Identifiable { ... }

// Step 4: Plan deep link URLs (if needed)
// myapp://recipe/{id}
// myapp://category/{name}/recipe/{id}

// Step 5: Plan state restoration (if needed)
// Will you use SceneStorage? What data must be Codable?

在实现导航前必须完成以下步骤
swift
// 步骤1:确定你的导航结构
// 思考:单栈?多列?每个标签带独立导航的标签式?
// 在编写任何代码前记录答案

// 步骤2:根据结构选择容器
// 单栈(以iPhone为主):NavigationStack
// 多列(以iPad/Mac为主):NavigationSplitView
// 标签式:TabView + 每个标签配NavigationStack

// 步骤3:定义导航使用的值类型
// 所有推入NavigationStack的值必须是Hashable
// 如需深度链接/状态恢复,还需是Codable
struct Recipe: Hashable, Codable, Identifiable { ... }

// 步骤4:规划深度链接URL(如有需要)
// myapp://recipe/{id}
// myapp://category/{name}/recipe/{id}

// 步骤5:规划状态恢复(如有需要)
// 是否使用SceneStorage?哪些数据需要是Codable?

Quick Decision Tree

快速决策树

Need navigation?
├─ Multi-column interface (iPad/Mac primary)?
│  └─ NavigationSplitView
│     ├─ Need drill-down in detail column?
│     │  └─ NavigationStack inside detail (Pattern 3)
│     └─ Selection-only detail?
│        └─ Just selection binding (Pattern 2)
├─ Tab-based app?
│  └─ TabView
│     ├─ Each tab needs drill-down?
│     │  └─ NavigationStack per tab (Pattern 4)
│     └─ iPad sidebar experience?
│        └─ .tabViewStyle(.sidebarAdaptable) (Pattern 5)
└─ Single-column stack?
   └─ NavigationStack
      ├─ Need deep linking?
      │  └─ Use NavigationPath (Pattern 1b)
      └─ Simple push/pop?
         └─ Typed array path (Pattern 1a)

Need state restoration?
└─ SceneStorage + Codable NavigationPath (Pattern 6)

Need coordinator abstraction?
├─ Complex conditional flows?
├─ Navigation logic testing needed?
├─ Sharing navigation across many screens?
└─ YES to any → Router pattern (Pattern 7)
   NO to all → Use NavigationPath directly

需要导航?
├─ 多列界面(以iPad/Mac为主)?
│  └─ NavigationSplitView
│     ├─ 详情列需要逐级深入?
│     │  └─ 在详情列内使用NavigationStack(模式3)
│     └─ 仅选择式详情?
│        └─ 仅使用选择绑定(模式2)
├─ 标签式应用?
│  └─ TabView
│     ├─ 每个标签需要逐级深入?
│     │  └─ 每个标签配NavigationStack(模式4)
│     └─ iPad侧边栏体验?
│        └─ .tabViewStyle(.sidebarAdaptable)(模式5)
└─ 单列栈?
   └─ NavigationStack
      ├─ 需要深度链接?
      │  └─ 使用NavigationPath(模式1b)
      └─ 简单的推送/弹出?
         └─ 类型化数组路径(模式1a)

需要状态恢复?
└─ SceneStorage + 可编码的NavigationPath(模式6)

需要协调器抽象?
├─ 复杂的条件流程?
├─ 需要测试导航逻辑?
├─ 多个页面共享导航?
└─ 以上任意为是 → 路由器模式(模式7)
   全部为否 → 直接使用NavigationPath

Pattern 1a: Basic NavigationStack

模式1a:基础NavigationStack

When: Simple push/pop navigation, all destinations same type
Time cost: 5-10 min
swift
struct RecipeList: View {
    @State private var path: [Recipe] = []

    var body: some View {
        NavigationStack(path: $path) {
            List(recipes) { recipe in
                NavigationLink(recipe.name, value: recipe)
            }
            .navigationTitle("Recipes")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        }
    }

    // Programmatic navigation
    func showRecipe(_ recipe: Recipe) {
        path.append(recipe)
    }

    func popToRoot() {
        path.removeAll()
    }
}
Key points:
  • Typed array
    [Recipe]
    when all values are same type
  • Value-based
    NavigationLink(title, value:)
  • navigationDestination(for:)
    outside lazy containers

适用场景:简单的推送/弹出导航,所有目标页面类型相同
时间成本:5-10分钟
swift
struct RecipeList: View {
    @State private var path: [Recipe] = []

    var body: some View {
        NavigationStack(path: $path) {
            List(recipes) { recipe in
                NavigationLink(recipe.name, value: recipe)
            }
            .navigationTitle("Recipes")
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetail(recipe: recipe)
            }
        }
    }

    // 程序化导航
    func showRecipe(_ recipe: Recipe) {
        path.append(recipe)
    }

    func popToRoot() {
        path.removeAll()
    }
}
关键点
  • 所有值类型相同时使用类型化数组
    [Recipe]
  • 使用基于值的
    NavigationLink(title, value:)
  • navigationDestination(for:)
    放置在惰性容器外部

Pattern 1b: NavigationStack with Deep Linking

模式1b:带深度链接的NavigationStack

When: Multiple destination types, URL-based deep linking
Time cost: 15-20 min
swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                .navigationDestination(for: Category.self) { category in
                    CategoryView(category: category)
                }
                .navigationDestination(for: Recipe.self) { recipe in
                    RecipeDetail(recipe: recipe)
                }
        }
        .onOpenURL { url in
            handleDeepLink(url)
        }
    }

    func handleDeepLink(_ url: URL) {
        // URL: myapp://category/desserts/recipe/apple-pie
        path.removeLast(path.count)  // Pop to root first

        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
        let segments = components.path.split(separator: "/").map(String.init)

        var index = 0
        while index < segments.count - 1 {
            switch segments[index] {
            case "category":
                if let category = Category(rawValue: segments[index + 1]) {
                    path.append(category)
                }
                index += 2
            case "recipe":
                if let recipe = dataModel.recipe(named: segments[index + 1]) {
                    path.append(recipe)
                }
                index += 2
            default:
                index += 1
            }
        }
    }
}
Key points:
  • NavigationPath
    for heterogeneous types
  • Pop to root before building deep link path
  • Build path in correct order (parent → child)

适用场景:多种目标页面类型,基于URL的深度链接
时间成本:15-20分钟
swift
struct ContentView: View {
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            HomeView()
                .navigationDestination(for: Category.self) { category in
                    CategoryView(category: category)
                }
                .navigationDestination(for: Recipe.self) { recipe in
                    RecipeDetail(recipe: recipe)
                }
        }
        .onOpenURL { url in
            handleDeepLink(url)
        }
    }

    func handleDeepLink(_ url: URL) {
        // URL: myapp://category/desserts/recipe/apple-pie
        path.removeLast(path.count)  // 先返回根视图

        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
        let segments = components.path.split(separator: "/").map(String.init)

        var index = 0
        while index < segments.count - 1 {
            switch segments[index] {
            case "category":
                if let category = Category(rawValue: segments[index + 1]) {
                    path.append(category)
                }
                index += 2
            case "recipe":
                if let recipe = dataModel.recipe(named: segments[index + 1]) {
                    path.append(recipe)
                }
                index += 2
            default:
                index += 1
            }
        }
    }
}
关键点
  • 异构类型使用
    NavigationPath
  • 构建深度链接路径前先返回根视图
  • 按正确顺序构建路径(父视图 → 子视图)

Pattern 2: NavigationSplitView Selection-Based

模式2:基于选择的NavigationSplitView

When: Multi-column layout where detail shows selected item
Time cost: 10-15 min
swift
struct MultiColumnView: View {
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.name, value: category)
            }
            .navigationTitle("Categories")
        } content: {
            if let category = selectedCategory {
                List(recipes(in: category), selection: $selectedRecipe) { recipe in
                    NavigationLink(recipe.name, value: recipe)
                }
                .navigationTitle(category.name)
            } else {
                Text("Select a category")
            }
        } detail: {
            if let recipe = selectedRecipe {
                RecipeDetail(recipe: recipe)
            } else {
                Text("Select a recipe")
            }
        }
    }
}
Key points:
  • selection: $binding
    on List connects to column selection
  • Value-presenting links update selection automatically
  • Adapts to single stack on iPhone

适用场景:多列布局,详情页显示选中的项目
时间成本:10-15分钟
swift
struct MultiColumnView: View {
    @State private var selectedCategory: Category?
    @State private var selectedRecipe: Recipe?

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.name, value: category)
            }
            .navigationTitle("Categories")
        } content: {
            if let category = selectedCategory {
                List(recipes(in: category), selection: $selectedRecipe) { recipe in
                    NavigationLink(recipe.name, value: recipe)
                }
                .navigationTitle(category.name)
            } else {
                Text("Select a category")
            }
        } detail: {
            if let recipe = selectedRecipe {
                RecipeDetail(recipe: recipe)
            } else {
                Text("Select a recipe")
            }
        }
    }
}
关键点
  • List上的
    selection: $binding
    关联列选择
  • 基于值的链接会自动更新选择状态
  • 在iPhone上自适应为单栈布局

Pattern 3: NavigationSplitView with Stack in Detail

模式3:详情列内带栈的NavigationSplitView

When: Multi-column with drill-down capability in detail
Time cost: 20-25 min
swift
struct GridWithDrillDown: View {
    @State private var selectedCategory: Category?
    @State private var path: [Recipe] = []

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.name, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $path) {
                if let category = selectedCategory {
                    RecipeGrid(category: category)
                        .navigationDestination(for: Recipe.self) { recipe in
                            RecipeDetail(recipe: recipe)
                        }
                } else {
                    Text("Select a category")
                }
            }
        }
    }
}
Key points:
  • NavigationStack inside detail column
  • Grid → Detail drill-down while preserving sidebar
  • Separate path for drill-down, selection for sidebar

适用场景:多列布局,详情页需要逐级深入
时间成本:20-25分钟
swift
struct GridWithDrillDown: View {
    @State private var selectedCategory: Category?
    @State private var path: [Recipe] = []

    var body: some View {
        NavigationSplitView {
            List(Category.allCases, selection: $selectedCategory) { category in
                NavigationLink(category.name, value: category)
            }
            .navigationTitle("Categories")
        } detail: {
            NavigationStack(path: $path) {
                if let category = selectedCategory {
                    RecipeGrid(category: category)
                        .navigationDestination(for: Recipe.self) { recipe in
                            RecipeDetail(recipe: recipe)
                        }
                } else {
                    Text("Select a category")
                }
            }
        }
    }
}
关键点
  • 在详情列内嵌入NavigationStack
  • 网格 → 详情的逐级深入,同时保留侧边栏
  • 逐级深入使用独立路径,侧边栏使用选择绑定

Pattern 4: TabView with Per-Tab NavigationStack

模式4:每个标签带NavigationStack的TabView

When: Tab-based app where each tab has its own navigation
Time cost: 15-20 min
swift
struct TabBasedApp: View {
    var body: some View {
        TabView {
            Tab("Home", systemImage: "house") {
                NavigationStack {
                    HomeView()
                        .navigationDestination(for: Item.self) { item in
                            ItemDetail(item: item)
                        }
                }
            }

            Tab("Search", systemImage: "magnifyingglass") {
                NavigationStack {
                    SearchView()
                }
            }

            Tab("Settings", systemImage: "gear") {
                NavigationStack {
                    SettingsView()
                }
            }
        }
    }
}
Key points:
  • Each Tab has its own NavigationStack
  • Navigation state preserved when switching tabs
  • iOS 18+ Tab syntax with systemImage

适用场景:标签式应用,每个标签拥有独立导航
时间成本:15-20分钟
swift
struct TabBasedApp: View {
    var body: some View {
        TabView {
            Tab("Home", systemImage: "house") {
                NavigationStack {
                    HomeView()
                        .navigationDestination(for: Item.self) { item in
                            ItemDetail(item: item)
                        }
                }
            }

            Tab("Search", systemImage: "magnifyingglass") {
                NavigationStack {
                    SearchView()
                }
            }

            Tab("Settings", systemImage: "gear") {
                NavigationStack {
                    SettingsView()
                }
            }
        }
    }
}
关键点
  • 每个Tab拥有独立的NavigationStack
  • 切换标签时保留导航状态
  • iOS 18+带systemImage的Tab语法

Pattern 5: Sidebar-Adaptable TabView (iOS 18+)

模式5:适配侧边栏的TabView(iOS 18+)

When: Tab bar on iPhone, sidebar on iPad
Time cost: 20-25 min
swift
struct AdaptableApp: View {
    var body: some View {
        TabView {
            Tab("Watch Now", systemImage: "play") {
                WatchNowView()
            }
            Tab("Library", systemImage: "books.vertical") {
                LibraryView()
            }

            TabSection("Collections") {
                Tab("Favorites", systemImage: "star") {
                    FavoritesView()
                }
                Tab("Recently Added", systemImage: "clock") {
                    RecentView()
                }
            }

            Tab(role: .search) {
                SearchView()
            }
        }
        .tabViewStyle(.sidebarAdaptable)
    }
}
Key points:
  • .tabViewStyle(.sidebarAdaptable)
    enables sidebar on iPad
  • TabSection
    creates collapsible groups in sidebar
  • Tab(role: .search)
    gets special placement

适用场景:iPhone显示标签栏,iPad显示侧边栏
时间成本:20-25分钟
swift
struct AdaptableApp: View {
    var body: some View {
        TabView {
            Tab("Watch Now", systemImage: "play") {
                WatchNowView()
            }
            Tab("Library", systemImage: "books.vertical") {
                LibraryView()
            }

            TabSection("Collections") {
                Tab("Favorites", systemImage: "star") {
                    FavoritesView()
                }
                Tab("Recently Added", systemImage: "clock") {
                    RecentView()
                }
            }

            Tab(role: .search) {
                SearchView()
            }
        }
        .tabViewStyle(.sidebarAdaptable)
    }
}
关键点
  • .tabViewStyle(.sidebarAdaptable)
    在iPad上启用侧边栏
  • TabSection
    在侧边栏中创建可折叠分组
  • Tab(role: .search)
    会获得特殊布局位置

Pattern 6: State Restoration

模式6:状态恢复

When: Preserve navigation state across app launches
Time cost: 25-30 min
swift
@MainActor
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe.ID] = []  // Store IDs, not objects

    enum CodingKeys: String, CodingKey {
        case selectedCategory, recipePath
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        try container.encode(recipePath, forKey: .recipePath)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
        recipePath = try container.decode([Recipe.ID].self, forKey: .recipePath)
    }

    init() {}

    var jsonData: Data? {
        get { try? JSONEncoder().encode(self) }
        set {
            guard let data = newValue,
                  let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
            else { return }
            selectedCategory = model.selectedCategory
            recipePath = model.recipePath
        }
    }
}

struct ContentView: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?

    var body: some View {
        NavigationStack(path: $navModel.recipePath) {
            // Content
        }
        .task {
            if let data { navModel.jsonData = data }
            for await _ in navModel.objectWillChange.values {
                data = navModel.jsonData
            }
        }
    }
}
Key points:
  • Store IDs, resolve to current objects
  • @MainActor
    for Swift 6 concurrency safety
  • SceneStorage for automatic scene-scoped persistence
  • Use
    compactMap
    when resolving IDs to handle deleted items

适用场景:跨应用启动保留导航状态
时间成本:25-30分钟
swift
@MainActor
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe.ID] = []  // 存储ID而非对象

    enum CodingKeys: String, CodingKey {
        case selectedCategory, recipePath
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
        try container.encode(recipePath, forKey: .recipePath)
    }

    required init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
        recipePath = try container.decode([Recipe.ID].self, forKey: .recipePath)
    }

    init() {}

    var jsonData: Data? {
        get { try? JSONEncoder().encode(self) }
        set {
            guard let data = newValue,
                  let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
            else { return }
            selectedCategory = model.selectedCategory
            recipePath = model.recipePath
        }
    }
}

struct ContentView: View {
    @StateObject private var navModel = NavigationModel()
    @SceneStorage("navigation") private var data: Data?

    var body: some View {
        NavigationStack(path: $navModel.recipePath) {
            // 内容
        }
        .task {
            if let data { navModel.jsonData = data }
            for await _ in navModel.objectWillChange.values {
                data = navModel.jsonData
            }
        }
    }
}
关键点
  • 存储ID,恢复时解析为当前对象
  • 使用
    @MainActor
    确保Swift 6并发安全
  • SceneStorage用于自动的场景级持久化
  • 解析ID时使用
    compactMap
    处理已删除的项目

Pattern 7: Router/Coordinator

模式7:路由器/协调器

When: Complex navigation logic, need testability
Time cost: 30-45 min
swift
enum AppRoute: Hashable {
    case home
    case category(Category)
    case recipe(Recipe)
    case settings
}

@Observable
@MainActor
class Router {
    var path = NavigationPath()

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

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

    func popToRoot() {
        path.removeLast(path.count)
    }

    func showRecipeOfTheDay() {
        popToRoot()
        if let recipe = DataModel.shared.recipeOfTheDay {
            path.append(AppRoute.recipe(recipe))
        }
    }
}

struct ContentView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .home: HomeView()
                    case .category(let cat): CategoryView(category: cat)
                    case .recipe(let recipe): RecipeDetail(recipe: recipe)
                    case .settings: SettingsView()
                    }
                }
        }
        .environment(router)
    }
}
When coordinators add value:
  • Complex conditional navigation flows
  • Navigation logic needs unit testing
  • Multiple views trigger same navigation
  • UIKit interop with custom transitions
When coordinators add complexity without value:
  • Simple linear navigation
  • < 5 navigation destinations
  • No need for navigation testing
  • NavigationPath already handles your deep linking

适用场景:复杂的导航逻辑,需要可测试性
时间成本:30-45分钟
swift
enum AppRoute: Hashable {
    case home
    case category(Category)
    case recipe(Recipe)
    case settings
}

@Observable
@MainActor
class Router {
    var path = NavigationPath()

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

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

    func popToRoot() {
        path.removeLast(path.count)
    }

    func showRecipeOfTheDay() {
        popToRoot()
        if let recipe = DataModel.shared.recipeOfTheDay {
            path.append(AppRoute.recipe(recipe))
        }
    }
}

struct ContentView: View {
    @State private var router = Router()

    var body: some View {
        NavigationStack(path: $router.path) {
            HomeView()
                .navigationDestination(for: AppRoute.self) { route in
                    switch route {
                    case .home: HomeView()
                    case .category(let cat): CategoryView(category: cat)
                    case .recipe(let recipe): RecipeDetail(recipe: recipe)
                    case .settings: SettingsView()
                    }
                }
        }
        .environment(router)
    }
}
协调器带来价值的场景
  • 复杂的条件导航流程
  • 导航逻辑需要单元测试
  • 多个视图触发相同的导航操作
  • 与UIKit互操作并使用自定义转场
协调器增加无价值复杂度的场景
  • 简单的线性导航
  • 导航目标页面少于5个
  • 无需测试导航逻辑
  • NavigationPath已能满足深度链接需求

Anti-Patterns (DO NOT DO THIS)

反模式(绝对不要这样做)

❌ Nesting NavigationStack inside NavigationStack

❌ 在NavigationStack内嵌套NavigationStack

swift
// ❌ WRONG — Nested stacks
NavigationStack {
    SomeView()
        .sheet(isPresented: $showSheet) {
            NavigationStack {  // Creates separate stack — confusing
                SheetContent()
            }
        }
}
Issue Two navigation stacks create confusing UX. Back button behavior unclear. Fix Use single NavigationStack, present sheets without nested navigation when possible.
swift
// ❌ 错误 — 嵌套栈
NavigationStack {
    SomeView()
        .sheet(isPresented: $showSheet) {
            NavigationStack {  // 创建独立栈 — 体验混乱
                SheetContent()
            }
        }
}
问题 两个导航栈会造成混乱的用户体验。返回按钮的行为不明确。 修复方案 使用单个NavigationStack,尽可能在不嵌套导航的情况下展示弹窗。

❌ Using NavigationLink inside Button

❌ 在Button内使用NavigationLink

swift
// ❌ WRONG — Double navigation triggers
Button("Go") {
    // Some action
} label: {
    NavigationLink(value: item) {  // Fires on button AND link
        Text("Item")
    }
}
Issue Both Button and NavigationLink respond to taps. Fix Use only NavigationLink, put action in
.simultaneousGesture
if needed.
swift
// ❌ 错误 — 触发双重导航
Button("Go") {
    // 某些操作
} label: {
    NavigationLink(value: item) {  // 按钮和链接都会响应点击
        Text("Item")
    }
}
问题 Button和NavigationLink都会响应点击。 修复方案 仅使用NavigationLink,如需操作可放在
.simultaneousGesture
中。

❌ Creating NavigationPath in view body

❌ 在视图body中创建NavigationPath

swift
// ❌ WRONG — Recreated every render
var body: some View {
    let path = NavigationPath()  // Reset on every render!
    NavigationStack(path: .constant(path)) { ... }
}
Issue Path recreated each render, navigation state lost. Fix Use
@State
or
@StateObject
for navigation state.

swift
// ❌ 错误 — 每次渲染都会重建
var body: some View {
    let path = NavigationPath()  // 每次渲染都会重置!
    NavigationStack(path: .constant(path)) { ... }
}
问题 每次渲染都会重建路径,导致导航状态丢失。 修复方案 使用
@State
@StateObject
存储导航状态。

Pressure Scenario: "Make Navigation Like Instagram"

压力场景:"实现类似Instagram的导航"

The Problem

问题

Product/design asks for complex navigation like Instagram:
  • "Tab bar with per-tab navigation stacks"
  • "Smooth coordinator pattern for all flows"
  • "Deep linking to any screen"
  • "Profile accessible from anywhere"
产品/设计要求实现类似Instagram的复杂导航:
  • "带独立导航栈的标签栏"
  • "所有流程使用流畅的协调器模式"
  • "支持深度链接到任意页面"
  • "从任何位置都能访问个人资料"

Red Flags — Recognize Over-Engineering Pressure

警示 — 识别过度设计的压力

If you hear ANY of these, STOP and evaluate:
  • 🚩 "Let's build a full coordinator layer before any views" → Usually YAGNI
  • 🚩 "We need a navigation architecture that handles anything" → Scope creep
  • 🚩 "Instagram/TikTok does it this way" → They have 100+ engineers
如果你听到以下任何说法,请立即停止并评估
  • 🚩 "在开发任何视图前先构建完整的协调器层" → 通常属于YAGNI(You Aren't Gonna Need It,你不会需要它)
  • 🚩 "我们需要一个能处理所有情况的导航架构" → 范围蔓延
  • 🚩 "Instagram/TikTok就是这么做的" → 他们有100+工程师

Time Cost Comparison

时间成本对比

Option A: Over-Engineered Coordinator

选项A:过度设计的协调器

  • Time to build coordinator layer: 3-5 days
  • Time to maintain and debug: Ongoing
  • Time when requirements change: Significant refactor
  • 构建协调器层时间:3-5天
  • 维护和调试时间:持续投入
  • 需求变更时的时间:大量重构

Option B: Built-in Navigation + Simple Router

选项B:内置导航 + 简单路由器

  • Time to implement Pattern 4 (TabView + NavigationStack): 2-3 hours
  • Time to add Router if needed: 1-2 hours
  • Time when requirements change: Incremental additions
  • 实现模式4(TabView + NavigationStack)时间:2-3小时
  • 如需添加路由器时间:1-2小时
  • 需求变更时的时间:增量式添加

How to Push Back Professionally

专业的反驳话术

Step 1: Quantify Current Needs

步骤1:量化当前需求

"Let's list our actual navigation flows:
1. Home → Item Detail
2. Search → Results → Item Detail
3. Profile → Settings

That's 6 destinations. NavigationPath handles this natively."
"我们先列出实际需要的导航流程:
1. 首页 → 项目详情
2. 搜索 → 结果 → 项目详情
3. 个人资料 → 设置

总共6个目标页面。NavigationPath原生就能处理这些。"

Step 2: Show the Built-in Solution

步骤2:展示内置解决方案

"Here's our navigation with NavigationStack + NavigationPath:
[Show Pattern 1b code]

This gives us:
- Programmatic navigation ✓
- Deep linking ✓
- State restoration ✓
- Type safety ✓

Without a coordinator layer."
"这是使用NavigationStack + NavigationPath实现的导航:
[展示模式1b代码]

它能提供:
- 程序化导航 ✓
- 深度链接 ✓
- 状态恢复 ✓
- 类型安全 ✓

无需协调器层。"

Step 3: Offer Incremental Path

步骤3:提供增量方案

"If we find NavigationPath insufficient, we can add a Router
(Pattern 7) later. It's 30-45 minutes of work.

But let's start with the simpler solution and add complexity
only when we hit a real limitation."
"如果发现NavigationPath不够用,我们之后可以添加路由器
(模式7),这只需要30-45分钟的工作量。

但我们先从更简单的方案开始,只有遇到真正的限制时再增加复杂度。"

Real-World Example: 48-Hour Feature Push

真实案例:48小时功能交付

Scenario:
  • PM: "We need deep linking for the campaign launch in 2 days"
  • Lead: "Let's build a proper coordinator first"
  • Time available: 16 working hours
Wrong approach:
  • 8 hours: Build coordinator infrastructure
  • 4 hours: Debug coordinator edge cases
  • 4 hours: Rush deep linking on broken foundation
  • Result: Buggy, deadline missed
Correct approach:
  • 2 hours: Implement Pattern 1b (NavigationStack with deep linking)
  • 1 hour: Test all deep link URLs
  • 1 hour: Add SceneStorage restoration (Pattern 6)
  • Result: Working deep links in 4 hours, 12 hours for polish/testing

场景
  • 产品经理:"我们需要在2天内为营销活动上线深度链接"
  • 负责人:"先构建一个合适的协调器"
  • 可用时间:16个工作小时
错误方案
  • 8小时:构建协调器基础设施
  • 4小时:调试协调器边缘情况
  • 4小时:在不稳定的基础上匆忙实现深度链接
  • 结果:Bug多,错过截止日期
正确方案
  • 2小时:实现模式1b(带深度链接的NavigationStack)
  • 1小时:测试所有深度链接URL
  • 1小时:添加SceneStorage恢复(模式6)
  • 结果:4小时内完成可用的深度链接,剩余12小时用于优化和测试

Pressure Scenario: "NavigationView Backward Compatibility"

压力场景:"NavigationView向后兼容"

The Problem

问题

Team lead says: "Let's use NavigationView so we support iOS 15"
团队负责人说:"我们使用NavigationView以支持iOS 15"

Red Flags

警示

  • 🚩 NavigationView deprecated since iOS 16 (2022)
  • 🚩 Different behavior across iOS versions causes bugs
  • 🚩 No NavigationPath support — can't deep link properly
  • 🚩 NavigationView自iOS 16起已被弃用(2022年)
  • 🚩 不同iOS版本间的行为差异会引发Bug
  • 🚩 不支持NavigationPath — 无法可靠实现深度链接

Data to Share

可分享的数据

iOS 16+ adoption: 95%+ of active devices (as of 2024)
iOS 15: < 5% and declining

NavigationView limitations:
- No programmatic path manipulation
- No type-safe navigation
- No built-in state restoration
- Behavior varies by iOS version
iOS 16+设备占比:95%+(截至2024年)
iOS 15:<5%且持续下降

NavigationView的局限性:
- 无程序化路径操作
- 无类型安全导航
- 无内置状态恢复
- 不同iOS版本行为不一致

Push-Back Script

反驳话术

"NavigationView was deprecated in iOS 16 (2022). Here's the impact:

1. We lose NavigationPath — can't implement deep linking reliably
2. Behavior differs between iOS 15 and 16 — more bugs to maintain
3. iOS 15 is < 5% of users — we're adding complexity for small audience

Recommendation: Set deployment target to iOS 16, use NavigationStack.
If iOS 15 support is required, use NavigationStack with @available
checks and fallback UI for older devices."

"NavigationView在iOS 16(2022年)已被弃用。影响如下:

1. 失去NavigationPath支持 — 无法可靠实现深度链接
2. iOS 15和16之间行为不同 — 需要维护更多Bug
3. iOS 15用户占比<5% — 我们为小众用户增加了复杂度

建议:将部署目标设为iOS 16,使用NavigationStack。
如果必须支持iOS 15,可使用NavigationStack并配合@available检查,为旧设备提供降级UI。"

Code Review Checklist

代码评审检查清单

Navigation Architecture

导航架构

  • Correct container for use case (Stack vs SplitView vs TabView)
  • Value-based NavigationLink (not view-based)
  • navigationDestination outside lazy containers
  • Each tab has own NavigationStack (if tab-based)
  • 根据使用场景选择了正确的容器(Stack vs SplitView vs TabView)
  • 使用了基于值的NavigationLink(而非基于视图)
  • navigationDestination放置在惰性容器外部
  • 标签式应用中每个标签拥有独立的NavigationStack

State Management

状态管理

  • NavigationPath in @State or @StateObject (not recreated in body)
  • @MainActor isolation for navigation state (Swift 6)
  • IDs stored for restoration (not full objects)
  • Error handling for decode failures
  • NavigationPath存储在@State或@StateObject中(而非在body中重建)
  • 导航状态使用@MainActor隔离(Swift 6)
  • 状态恢复存储的是ID(而非完整对象)
  • 处理了解码失败的情况

Deep Linking

深度链接

  • onOpenURL handler present
  • Pop to root before building path
  • Path built in correct order (parent → child)
  • Missing data handled gracefully
  • 存在onOpenURL处理器
  • 构建路径前先返回根视图
  • 按正确顺序构建路径(父视图 → 子视图)
  • 优雅处理缺失的数据

iOS 26+ Features

iOS 26+功能

  • No custom backgrounds interfering with Liquid Glass
  • Bottom-aligned search working on iPhone
  • Tab bar minimization if appropriate

  • 无自定义背景干扰Liquid Glass
  • iPhone上的底部搜索正常工作
  • 按需启用标签栏最小化

Troubleshooting Quick Reference

故障排查速查表

SymptomLikely CausePattern
Navigation doesn't respond to tapsNavigationLink outside NavigationStackCheck hierarchy
Double navigation on tapButton wrapping NavigationLinkRemove Button wrapper
State lost on tab switchShared NavigationStack across tabsPattern 4
State lost on backgroundNo SceneStoragePattern 6
Deep link shows wrong screenPath built in wrong orderPattern 1b
Crash on restoreForce unwrap decodeHandle errors gracefully

症状可能原因对应模式
导航不响应点击NavigationLink在NavigationStack外部检查层级结构
点击触发双重导航Button包裹了NavigationLink移除Button包裹
切换标签时状态丢失所有标签共享同一个NavigationStack模式4
进入后台后状态丢失未使用SceneStorage模式6
深度链接显示错误页面路径构建顺序错误模式1b
恢复时崩溃强制解包解码结果优雅处理错误

Resources

资源

WWDC: 2022-10054, 2024-10147, 2025-256, 2025-323
Skills: axiom-swiftui-nav-diag, axiom-swiftui-nav-ref

Last Updated Based on WWDC 2022-2025 navigation sessions Platforms iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, tvOS 18+
WWDC: 2022-10054, 2024-10147, 2025-256, 2025-323
技能: axiom-swiftui-nav-diag, axiom-swiftui-nav-ref

最后更新 基于WWDC 2022-2025导航相关会话 平台 iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, tvOS 18+