The SwiftUI cookbook for navigation
Swift & UI 进阶 20m

SwiftUI 导航 cookbook

The SwiftUI cookbook for navigation

2022年6月6日

在 Apple 官方观看视频

一句话判断

NavigationStack 和 NavigationSplitView 终于让 SwiftUI 的导航系统有了数据驱动和编程控制的能力——如果你还在用老的 NavigationLink API,该升级了。

这场 Session 讲了什么

SwiftUI 引入了全新的数据驱动导航 API,Session 以”烹饪食谱应用”为示例,逐步展示了三种导航结构的实现:

NavigationStack:单列 push-pop 导航,类似 iOS Settings。核心是一个 path 绑定——一个集合类型,代表当前栈上所有已推送的数据值。修改 path 就能实现深链接和编程式导航。

NavigationSplitView:多列导航,类似 Mail 或 Notes。支持两列和三列两种配置,在 iPhone 和 Slide Over 上自动退化为单列堆栈。

NavigationLink 的新变体:不再直接绑定 View,而是绑定一个值(value)。配合 navigationDestination 修饰符声明值到视图的映射关系。

整个系统的工作原理:当用户点击 NavigationLink 时,值被追加到 path;NavigationStack 根据 path 中的值和已注册的 destination 修饰符,决定推送哪个视图。

值得深挖的点

Path 绑定的编程式控制是这次更新最大的卖点。过去要做深链接,你需要为每个 NavigationLink 维护独立的 binding。现在 path 就是一个普通数组——要跳转到特定页面,直接设置 path 内容;要回到根视图,清空 path 即可。这让 URL 路由和状态恢复变得异常简单。

navigationDestination 的类型分发机制。一个 navigationDestination(for: Recipe.self) 修饰符会处理所有类型为 Recipe 的值。如果你需要在一个栈中展示多种类型,可以注册多个 destination 修饰符,SwiftUI 根据值的类型自动路由。

NavigationSplitView 的自适应行为值得理解。在 iPad 上它是三列布局(sidebar / content / detail),在 iPhone 上自动变成单列堆栈。这种自适应不需要你写任何条件代码,SwiftUI 根据屏幕尺寸自动处理。

代码片段

基础 NavigationStack:

struct RecipeListView: View {
    // path 是一个数组,存储所有已推送的 Recipe
    @State var path = [Recipe]()
    
    var body: some View {
        NavigationStack(path: $path) {
            List(categories) { category in
                Section(category.name) {
                    ForEach(category.recipes) { recipe in
                        // 新版 NavigationLink 只绑定值
                        NavigationLink(recipe.name, value: recipe)
                    }
                }
            }
            .navigationTitle("食谱")
            // 声明值到视图的映射
            .navigationDestination(for: Recipe.self) { recipe in
                RecipeDetailView(recipe: recipe)
            }
        }
    }
}

编程式导航和深链接:

// 深链接:直接设置 path
func deepLink(to recipe: Recipe) {
    path = [recipe]
}

// 回到根视图
func popToRoot() {
    path.removeAll()
}

// 回退一步
func popOne() {
    path.removeLast()
}

三列 NavigationSplitView:

NavigationSplitView {
    // 第一列:分类列表
    List(categories, selection: $selectedCategory)
} content: {
    // 第二列:食谱列表
    List(selectedCategory?.recipes ?? []) { recipe in
        NavigationLink(recipe.name, value: recipe)
    }
} detail: {
    // 第三列:食谱详情
    if let recipe = selectedRecipe {
        RecipeDetailView(recipe: recipe)
    }
}

最佳实践

  • 用新变体 NavigationLink(value:):不要在 Link 中直接嵌入目标视图,分离数据和视图
  • Path 类型用具体类型:单类型栈用 [Recipe],多类型栈用 NavigationPath
  • destination 修饰符放在合适位置:放在栈内或根视图上,确保所有层级都能路由
  • NavigationSplitView 配置用修饰符:columnVisibility 和 columnWidths 都有对应的修饰符
  • 持久化 path 用 Codable:NavigationPath 支持 codable 转换,可以保存和恢复导航状态

还有什么值得关注

  • NavigationSplitView 的列配置选项非常丰富,建议看 “SwiftUI on iPad: Organize your interface” 了解详情
  • 旧的 NavigationLink(view:) API 继续可用,但新的值绑定 API 是未来方向
  • NavigationPath 是类型擦除的路径容器,适合需要混合多种数据类型的复杂导航场景
  • Session 的 cookbook 隐喻贯穿全场,代码示例都是围绕食谱应用展开的
WWDC 2022