关于 axiom-swiftui-nav-ref
面向 SwiftUI 开发者的导航 API 参考指南,介绍 NavigationStack、NavigationSplitView、NavigationPath 与 Tab 集成。还涵盖深层链接、状态恢复和 API 演进,帮助实现从单列到多列的导航结构。
主要功能
- 01NavigationStack 与 NavigationPath 用法
- 02自适应多列导航布局
- 03深层链接与 URL 路由
- 04导航状态持久化与恢复
- 05Tab 与导航栈集成
适用场景
- 01实现类型安全的程序化页面跳转
- 02为 iPhone 和 iPad 设计自适应导航
- 03根据 URL 打开应用内指定页面
- 04在应用重启后恢复导航位置
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ charleswiltgen/ axiom --skill axiom-swiftui-nav-ref安装后在 Claude Code 中输入/axiom-swiftui-nav-ref确认已加载。
详细介绍
SwiftUI 导航 API 参考
概述
SwiftUI 的导航 API 提供了由数据驱动的编程式导航,既适用于简单的堆栈,也能扩展到复杂的多列布局。该 API 于 iOS 16(2022 年)随 NavigationStack 和 NavigationSplitView 引入,在 iOS 18(2024 年)通过 Tab/Sidebar 统一得到扩展,并在 iOS 26(2025 年)借助 Liquid Glass 设计进一步完善。
演进时间线
- 2022 年(iOS 16) NavigationStack、NavigationSplitView、NavigationPath、基于值的 NavigationLink
- 2024 年(iOS 18) Tab/Sidebar 统一、sidebarAdaptable 样式、缩放导航过渡
- 2025 年(iOS 26) Liquid Glass 导航栏外观、底部对齐的搜索、浮动标签栏、backgroundExtensionEffect
主要功能
- 数据驱动导航 NavigationPath 表示堆栈状态,可执行编程式 push/pop 和深度链接
- 多列布局 NavigationSplitView 可自动适配(iPad 上为 3 列 → iPhone 上为单列堆栈)
- 状态恢复 使用 Codable NavigationPath + SceneStorage,在应用启动之间持久化状态
- 标签页集成 每个标签页使用独立的 NavigationStack,并在切换标签页时保留状态(iOS 18+)
- Liquid Glass 自动为导航栏、侧边栏和工具栏应用玻璃效果(iOS 26+)
何时使用 SwiftUI,而非 UIKit
- SwiftUI 导航 新应用、多平台应用、较简单的导航流程 → 使用 NavigationStack/SplitView
- UINavigationController 复杂的协调器模式、旧代码、特定 UIKit 功能 → 考虑使用 UIKit
相关技能
- 使用
axiom-swiftui-nav了解反模式、决策树和压力场景 - 使用
axiom-swiftui-nav-diag系统性排查导航问题
何时使用此技能
在以下情况下使用此技能:
- 实现导航 API NavigationStack、NavigationSplitView、NavigationPath、Tab+Navigation
- 深度链接或状态恢复 URL 路由、Codable NavigationPath、SceneStorage
- 采用 iOS 26+ 功能 Liquid Glass 导航、底部对齐的搜索、标签栏最小化
- 选择导航架构 Stack、SplitView 或协调器模式
API 演进
时间线
| 年份 | iOS 版本 | 主要功能 |
|---|---|---|
| 2020 | iOS 14 | NavigationView(iOS 16 起已弃用) |
| 2022 | iOS 16 | NavigationStack、NavigationSplitView、NavigationPath、基于值的 NavigationLink |
| 2024 | iOS 18 | Tab/Sidebar 统一、sidebarAdaptable、TabSection、缩放过渡 |
| 2025 | iOS 26 | Liquid Glass 导航、backgroundExtensionEffect、tabBarMinimizeBehavior |
NavigationView(已弃用)
NavigationView 自 iOS 16 起已弃用。新代码中应仅使用 NavigationStack(单列 push/pop)或 NavigationSplitView(多列)。主要改进包括:用单个 NavigationPath 取代每个链接的 isActive 绑定、提供基于值的类型安全,以及内置 Codable 状态恢复。请参阅“迁移到新导航类型”文档。
NavigationStack 完整参考
NavigationStack 表示一种 push-pop 界面,例如 iPhone 上的“设置”或 macOS 上的“系统设置”。
1.1 创建 NavigationStack
基本 NavigationStack
NavigationStack {
List(Category.allCases) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categories")
.navigationDestination(for: Category.self) { category in
CategoryDetail(category: category)
}
}
使用路径绑定(WWDC 2022,6:05)
struct PushableStack: View {
@State private var path: [Recipe] = []
@StateObject private var dataModel = DataModel()
var body: some View {
NavigationStack(path: $path) {
List(Category.allCases) { category in
Section(category.localizedName) {
ForEach(dataModel.recipes(in: category)) { recipe in
NavigationLink(recipe.name, value: recipe)
}
}
}
.navigationTitle("Categories")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
.environmentObject(dataModel)
}
}
路径绑定 + 呈现值的 NavigationLink + navigationDestination(for:) 构成数据驱动导航的核心模式。
1.2 NavigationLink(基于值)
呈现值的 NavigationLink
// Correct: Value-based (iOS 16+)
NavigationLink(recipe.name, value: recipe)
// Correct: With custom label
NavigationLink(value: recipe) {
RecipeTile(recipe: recipe)
}
// Deprecated: View-based (iOS 13-15)
NavigationLink(recipe.name) {
RecipeDetail(recipe: recipe) // Don't use in new code
}
NavigationLink 如何与 NavigationStack 配合
- NavigationStack 维护一个
path集合 - 点击呈现值的链接会将该值追加到路径中
- NavigationStack 会将
navigationDestination修饰符映射到路径中的值 - 视图会根据目标映射推入堆栈
1.3 navigationDestination 修饰符
单一类型
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
多种类型
NavigationStack(path: $path) {
RootView()
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
.navigationDestination(for: Category.self) { category in
CategoryList(category: category)
}
.navigationDestination(for: Chef.self) { chef in
ChefProfile(chef: chef)
}
}
导航反模式
- 切勿在同一个 NavigationStack 层级中混用
navigationDestination(for:)和NavigationLink(destination:)——这会导致未定义行为 - 每种数据类型只注册一次
navigationDestination(for:)——重复注册会导致显示错误的视图
放置规则
- 将
navigationDestination放在惰性容器之外(不要放在 ForEach 内) - 将其放在相关 NavigationLinks 附近,以便整理代码
- 必须位于 NavigationStack 层级内
// Correct: Outside lazy container
ScrollView {
LazyVGrid(columns: columns) {
ForEach(recipes) { recipe in
NavigationLink(value: recipe) {
RecipeTile(recipe: recipe)
}
}
}
}
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
// Wrong: Inside ForEach (may not be loaded)
ForEach(recipes) { recipe in
NavigationLink(value: recipe) { RecipeTile(recipe: recipe) }
.navigationDestination(for: Recipe.self) { r in // Don't do this
RecipeDetail(recipe: r)
}
}
1.4 NavigationPath
NavigationPath 是一个类型擦除集合,用于异构导航栈。
类型化数组与 NavigationPath
// Typed array: All values same type
@State private var path: [Recipe] = []
// NavigationPath: Mixed types
@State private var path = NavigationPath()
NavigationPath 操作
// Append value
path.append(recipe)
// Pop to previous
path.removeLast()
// Pop to root
path.removeLast(path.count)
// or
path = NavigationPath()
// Check count
if path.count > 0 { ... }
// Deep link: Set multiple values
path.append(category)
path.append(recipe)
Codable 支持
// NavigationPath is Codable when all values are Codable
@State private var path = NavigationPath()
// Encode
let data = try JSONEncoder().encode(path.codable)
// Decode
let codableRep = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
path = NavigationPath(codableRep)
NavigationSplitView 完整参考
NavigationSplitView 会创建可适应设备尺寸的多列布局。
2.1 两列布局
基础两列布局(WWDC 2022,10:40)
struct MultipleColumns: View {
@State private var selectedCategory: Category?
@State private var selectedRecipe: Recipe?
@StateObject private var dataModel = DataModel()
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.localizedName, value: category)
}
.navigationTitle("Categories")
} detail: {
if let recipe = selectedRecipe {
RecipeDetail(recipe: recipe)
} else {
Text("Select a recipe")
}
}
}
}
2.2 三列布局
在侧边栏和详情之间添加一个 content: 闭包作为中间列:
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.localizedName, value: category)
}
} content: {
List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
NavigationLink(recipe.name, value: recipe)
}
} detail: {
RecipeDetail(recipe: selectedRecipe)
}
2.3 将 NavigationSplitView 与 NavigationStack 配合使用
在详情列中放置一个 NavigationStack(path:),即可在保留侧边栏所选项的同时,从网格逐层导航到详情:
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { ... }
} detail: {
NavigationStack(path: $path) {
RecipeGrid(category: selectedCategory)
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
}
2.4 列可见性
@State private var columnVisibility: NavigationSplitViewVisibility = .all
NavigationSplitView(columnVisibility: $columnVisibility) {
Sidebar()
} content: {
Content()
} detail: {
Detail()
}
// Programmatically control visibility
columnVisibility = .detailOnly // Hide sidebar and content
columnVisibility = .all // Show all columns
columnVisibility = .automatic // System decides
2.5 自动适配
NavigationSplitView 会自动适配:
- iPad 横屏 显示所有列(取决于配置)
- iPad 竖屏/Slide Over 折叠为覆盖层或单列
- iPhone 单一导航栈
- Apple Watch/TV 单一导航栈
在 iPhone 上,选择变化会自动转换为推入/弹出操作。
2.6 iOS 26+ Liquid Glass 侧边栏(WWDC 2025,323)
NavigationSplitView {
List { ... }
} detail: {
DetailView()
}
// Sidebar automatically gets Liquid Glass appearance on iPad/macOS
// Extend content behind glass sidebar
.backgroundExtensionEffect() // Mirrors and blurs content outside safe area
深度链接与 URL 路由
3.1 深度链接模式
使用 .onOpenURL 接收 URL,使用 URLComponents 解析,然后操作 NavigationPath:
.onOpenURL { url in
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let host = components.host else { return }
path.removeLast(path.count) // Pop to root first
// Parse host/path to determine destination, then path.append(value)
}
对于多步深度链接(myapp://category/desserts/recipe/apple-pie),遍历 URL 路径组件,并将每个解析出的值追加到导航栈,以构建完整导航栈。
如需查看全面的深度链接示例、错误诊断和测试工作流,请参阅 axiom-swiftui-nav-diag(模式 3)。
状态恢复
4.1 完整状态恢复(WWDC 2022,18:12)
struct UseSceneStorage: View {
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
@StateObject private var dataModel = DataModel()
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $navModel.selectedCategory) { category in
NavigationLink(category.localizedName, value: category)
}
.navigationTitle("Categories")
} detail: {
NavigationStack(path: $navModel.recipePath) {
RecipeGrid(category: navModel.selectedCategory)
}
}
.task {
// Restore on appear
if let data = data {
navModel.jsonData = data
}
// Save on changes
for await _ in navModel.objectWillChangeSequence {
data = navModel.jsonData
}
}
.environmentObject(dataModel)
}
}
4.2 Codable NavigationModel
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe] = []
enum CodingKeys: String, CodingKey {
case selectedCategory
case recipePathIds // Store IDs, not full objects
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
try container.encode(recipePath.map(\.id), forKey: .recipePathIds)
}
init() {}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] } // Discard deleted items
}
}
存储 ID(而不是完整模型对象),并使用 compactMap 优雅地处理已删除的项目。添加 jsonData 计算属性和 objectWillChangeSequence,以便与 SceneStorage 集成,如 4.1 所示。
Tab 与导航集成
5.1 Tab 语法(iOS 18+)(WWDC 2024,4:27)
TabView {
Tab("Watch Now", systemImage: "play") {
WatchNowView()
}
Tab("Library", systemImage: "books.vertical") {
LibraryView()
}
Tab(role: .search) {
NavigationStack {
SearchView()
.navigationTitle("Search")
}
.searchable(text: $searchText)
}
}
搜索 Tab 要求:搜索角色 Tab 的内容必须包装在 NavigationStack 中,并对该栈应用 .searchable()。如果没有 NavigationStack,搜索字段将不会显示。如需了解基础 .searchable 模式(建议、范围、令牌、程序化控制),请参阅 axiom-swiftui-search-ref。
5.2 每个 Tab 使用一个 NavigationStack 的 TabView
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
HomeView()
.navigationDestination(for: Item.self) { item in
ItemDetail(item: item)
}
}
}
Tab("Settings", systemImage: "gear") {
NavigationStack {
SettingsView()
}
}
}
每个 Tab 都有自己的 NavigationStack,以便在切换 Tab 时保留导航状态。
5.3 侧边栏适配型 TabView(WWDC 2024,6:41)
TabView {
Tab("Watch Now", systemImage: "play") {
WatchNowView()
}
Tab("Library", systemImage: "books.vertical") {
LibraryView()
}
TabSection("Collections") {
Tab("Cinematic Shots", systemImage: "list.and.film") {
CinematicShotsView()
}
Tab("Forest Life", systemImage: "list.and.film") {
ForestLifeView()
}
}
TabSection("Animations") {
// More tabs...
}
Tab(role: .search) {
SearchView()
}
}
.tabViewStyle(.sidebarAdaptable)
TabSection 用于创建侧边栏分组。.sidebarAdaptable 会在 iPad 上启用侧边栏,在 iPhone 上启用标签栏。带有 .search 角色的搜索 Tab 会获得特殊位置。
5.4 Tab 自定义(WWDC 2024,10:45)
@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization
TabView {
Tab("Watch Now", systemImage: "play", value: .watchNow) {
WatchNowView()
}
.customizationID("Tab.watchNow")
.customizationBehavior(.disabled, for: .sidebar, .tabBar) // Can't be hidden
Tab("Optional Tab", systemImage: "star", value: .optional) {
OptionalView()
}
.customizationID("Tab.optional")
.defaultVisibility(.hidden, for: .tabBar) // Hidden by default
}
.tabViewCustomization($customization)
5.5 Tab 上下文菜单
在 Tab 上使用 .contextMenu(menuItems:),可为其侧边栏表示添加上下文菜单(例如,在 Mac 上右键单击,或在 iPad 侧边栏上长按)。
Tab("Currently Reading", systemImage: "book") {
CurrentBooksList()
}
.contextMenu {
Button {
pinnedTabs.insert(.reading)
} label: {
Label("Pin", systemImage: "pin")
}
Button {
showShareSheet = true
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
}
返回一个空闭包,以有条件地停用上下文菜单:
.contextMenu {
if canPin {
Button("Pin", systemImage: "pin") { pin() }
}
}
iPhone 标签栏长按
Tab 上的 .contextMenu 仅适用于侧边栏表示形式(iPad/Mac)。iPhone 标签栏上下文菜单需要与 UIKit 互操作(通过 Introspect 或 UITabBarController 子类向 UITabBar 添加 UILongPressGestureRecognizer)。变通方案模式请参阅 axiom-swiftui-nav-diag。
注意:依赖私有的 UITabBarButton 子视图,在不同 iOS 版本间可能不稳定,也不属于公开 API 保证的行为。
5.6 以编程方式控制标签可见性
使用 .hidden(_:) 根据应用状态显示或隐藏标签,同时保留其导航状态。
由状态驱动的标签可见性
Tab("Libraries", systemImage: "square.stack") { LibrariesView() }
.hidden(context == .browse) // Hide based on app state
对每个标签应用 .hidden(condition)。通过这种方式隐藏的标签会保留其导航状态(而条件式 if 渲染会销毁并重新创建标签)。
状态保留
主要区别:.hidden(_:) 会保留标签状态,而条件式渲染不会。
// ✅ State preserved when hidden
Tab("Settings", systemImage: "gear") {
SettingsView() // Navigation stack preserved
}
.hidden(!showSettings)
// ❌ State lost when condition changes
if showSettings {
Tab("Settings", systemImage: "gear") {
SettingsView() // Navigation stack recreated
}
}
常见模式
Tab("Beta Features", systemImage: "flask") { BetaView() }
.hidden(!UserDefaults.standard.bool(forKey: "enableBetaFeatures"))
身份验证状态、购买状态和调试版本也适用相同模式——将 .hidden() 绑定到任何布尔条件即可。
动画过渡
使用 withAnimation 包裹状态变更,实现流畅的标签栏布局过渡:
Button("Switch to Browse") {
withAnimation {
context = .browse
selection = .tracks // Switch to first visible tab
}
}
// Tab bar animates as tabs appear/disappear
// Uses system motion curves automatically
5.7 iOS 26+ 标签功能(WWDC 2025,256)
// Tab bar minimization on scroll
TabView { ... }
.tabBarMinimizeBehavior(.onScrollDown)
// Bottom accessory view (always visible)
TabView { ... }
.tabViewBottomAccessory {
PlaybackControls()
}
// Dynamic visibility (recommended for mini-players)
// ⚠️ Requires iOS 26.1+ (not 26.0)
TabView { ... }
.tabViewBottomAccessory(isEnabled: showMiniPlayer) {
MiniPlayerView()
.transition(.opacity)
}
// isEnabled: true = shows accessory
// isEnabled: false = hides AND removes reserved space
// Search tab with dedicated search field
Tab(role: .search) {
NavigationStack {
SearchView()
.navigationTitle("Search")
}
.searchable(text: $searchText)
}
// Morphs into search field when selected
// ⚠️ NavigationStack wrapper required for search field to appear
// Fallback: If no tab has .search role, the tab view applies search
// to ALL tabs, resetting search state when the selected tab changes
动态底部配件
配件可以根据 activeTab 切换内容以适配不同标签,不过 Apple 的用法(Music 迷你播放器)是全局显示。读取 @Environment(\.tabViewBottomAccessoryPlacement) 以调整布局:位于标签栏上方时为 .bar(完整控件),与折叠标签栏内联时则为其他值(紧凑布局)。
将 tabViewBottomAccessory 用于跨标签内容(播放、状态)。对于特定标签的操作,优先在标签的内容视图中使用浮动玻璃按钮。
5.8 标签 API 快速参考
| 修饰符 | 目标 | iOS | 用途 |
|---|---|---|---|
Tab(_:systemImage:value:content:) | — | 18+ | 使用选择值的新标签语法 |
Tab(role: .search) | — | 18+ | 具有变形行为的语义搜索标签 |
TabSection(_:content:) | — | 18+ | 在侧边栏视图中将标签分组 |
.contextMenu(menuItems:) | Tab | 18+ | 为标签的侧边栏表示形式添加上下文菜单 |
.customizationID(_:) | Tab | 18+ | 启用用户自定义 |
.customizationBehavior(_:for:) | Tab | 18+ | 控制隐藏/重新排序权限 |
.defaultVisibility(_:for:) | Tab | 18+ | 设置初始可见状态 |
.hidden(_:) | Tab | 18+ | 以编程方式控制可见性并保留状态 |
.tabViewStyle(.sidebarAdaptable) | TabView | 18+ | iPad 上显示侧边栏,iPhone 上显示标签 |
.tabViewCustomization($binding) | TabView | 18+ | 持久化用户的标签排列 |
.tabBarMinimizeBehavior(_:) | TabView | 26+ | 滚动时自动隐藏 |
.tabViewBottomAccessory(isEnabled:content:) | TabView | 26.1+ | 在标签栏下方显示动态内容 |
iOS 26+ 导航功能
6.1 Liquid Glass 导航(WWDC 2025,323)
使用 Xcode 26 构建时会自动采用:
- 导航栏变为 Liquid Glass
- 侧边栏以玻璃效果浮于内容上方
- 标签栏以新的紧凑外观浮动显示
- 工具栏自动分组
6.2 背景延伸效果
NavigationSplitView {
Sidebar()
} detail: {
HeroImage()
.backgroundExtensionEffect() // Content extends behind sidebar
}
6.3 底部对齐搜索(WWDC 2025,256)
搜索基础 API 有关 .searchable、isSearching、建议、范围、令牌和编程式控制,请参阅 axiom-swiftui-search-ref。本节仅介绍 iOS 26 中底部对齐的改进。
NavigationSplitView {
Sidebar()
} detail: {
DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad
6.4 滚动边缘效果
// Automatic blur effect when content scrolls under toolbar
// Remove any custom darkening backgrounds - they interfere
// For dense UIs, adjust sharpness
ScrollView { ... }
.scrollEdgeEffectStyle(.soft) // .sharp, .soft
6.5 使用缩放转场呈现工作表
在 iOS 26 中,工作表可以直接从呈现它们的按钮变形而出。将呈现工作表的工具栏项设为导航缩放转场的源,并将工作表内容标记为目标:
@Namespace private var namespace
// Sheet morphs out of presenting button
.toolbar {
ToolbarItem {
Button("Settings") { showSettings = true }
.matchedTransitionSource(id: "settings", in: namespace)
}
}
.sheet(isPresented: $showSettings) {
SettingsView()
.navigationTransition(.zoom(sourceID: "settings", in: namespace))
}
其他呈现形式也能从 Liquid Glass 控件中流畅弹出,包括菜单、警告框和弹出框。对话框会自动从呈现它们的按钮变形而出,无需额外代码。
审查提示:如果你曾使用 presentationBackground 为工作表应用自定义背景,可以考虑移除它,让新的 Liquid Glass 工作表材质充分展现。现在,默认情况下,部分高度的工作表会采用内嵌布局,并带有玻璃背景。
6.6 工具栏变形转场
在 iOS 26 中,当每个目标视图都声明自己的 .toolbar {} 时,系统会在 NavigationStack 推入或弹出视图期间自动为工具栏添加变形转场。具有匹配 toolbar(id:) 和 ToolbarItem(id:) ID 的项在转场期间保持稳定(不会弹跳),而未匹配的项则会以动画形式出现或消失。
关键规则:将 .toolbar {} 附加到 NavigationStack 内的各个视图,而不是 NavigationStack 本身。否则就没有可供变形衔接的内容。
完整的工具栏变形 API(包括 DefaultToolbarItem、toolbar(id:) 稳定项、ToolbarSpacer 模式和故障排查)请参阅 axiom-swiftui-26-ref 技能。
Router/Coordinator 模式
7.1 何时使用 Coordinator
适合使用 Coordinator 的情况:
- 导航逻辑复杂,包含条件流程
- 需要单独测试导航
- 需要在多个屏幕之间共享导航逻辑
- UIKit 互操作需要大量导航功能
适合使用内置导航的情况:
- 简单的线性或分层导航
- 状态恢复是首要考虑因素
- 导航目标少于 5-10 个
- 不需要对导航进行单元测试
7.2 简单 Router 模式
// Route enum defines all possible destinations
enum AppRoute: Hashable {
case home
case category(Category)
case recipe(Recipe)
case settings
}
// Router class manages navigation
@Observable
class Router {
var path = NavigationPath()
func navigate(to route: AppRoute) {
path.append(route)
}
func popToRoot() {
path.removeLast(path.count)
}
func pop() {
if !path.isEmpty {
path.removeLast()
}
}
}
// Usage in views
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 category):
CategoryView(category: category)
case .recipe(let recipe):
RecipeDetail(recipe: recipe)
case .settings:
SettingsView()
}
}
}
.environment(router)
}
}
// In child views
struct RecipeCard: View {
let recipe: Recipe
@Environment(Router.self) private var router
var body: some View {
Button(recipe.name) {
router.navigate(to: .recipe(recipe))
}
}
}
7.3 使用协议的 Coordinator 模式
对于较大的应用,可以提取一个 Coordinator 协议,其中包含 associatedtype Route: Hashable 和 var path: NavigationPath。每个功能区域都可以遵循该协议,并定义自己的领域专属路由和便捷方法(例如 showRecipeOfTheDay(),用于重置路径并执行导航)。
7.4 测试导航
// Router is easily testable
func testNavigateToRecipe() {
let router = Router()
let recipe = Recipe(name: "Apple Pie")
router.navigate(to: .recipe(recipe))
XCTAssertEqual(router.path.count, 1)
}
func testPopToRoot() {
let router = Router()
router.navigate(to: .category(.desserts))
router.navigate(to: .recipe(Recipe(name: "Apple Pie")))
router.popToRoot()
XCTAssertTrue(router.path.isEmpty)
}
测试清单
- 深度链接在冷启动时以及应用运行期间都能正确导航
- 弹回根视图会清除整个栈
- 应用重新启动后状态能够恢复(每个场景的 SceneStorage 键都唯一)
- 在恢复过程中妥善处理已删除的项(compactMap)
- NavigationSplitView 在 iPhone 上正确折叠(选择项会推入视图)
- iOS 26+:Liquid Glass 外观、底部对齐的搜索栏、标签栏最小化
API 快速参考
NavigationStack
NavigationStack { content }
NavigationStack(path: $path) { content }
NavigationSplitView
NavigationSplitView { sidebar } detail: { detail }
NavigationSplitView { sidebar } content: { content } detail: { detail }
NavigationSplitView(columnVisibility: $visibility) { ... }
NavigationLink
NavigationLink(title, value: value)
NavigationLink(value: value) { label }
NavigationPath
path.append(value)
path.removeLast()
path.removeLast(path.count)
path.count
path.codable // For encoding
NavigationPath(codableRepresentation) // For decoding
修饰符
.navigationTitle("Title")
.navigationDestination(for: Type.self) { value in View }
.searchable(text: $query)
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.backgroundExtensionEffect()
资源
WWDC:2022-10054、2024-10147、2025-256、2025-323(使用新设计构建 SwiftUI 应用)
文档:/swiftui/tabrole/search、/swiftui/view/tabbarminimizebehavior(_:)、/swiftui/view/tabviewbottomaccessory(isenabled:content:)
技能:axiom-swiftui-nav、axiom-swiftui-nav-diag、axiom-swiftui-26-ref、axiom-liquid-glass、axiom-swiftui-search-ref
最后更新 基于 WWDC 2022-10054、WWDC 2024-10147、WWDC 2025-256、WWDC 2025-323(使用新设计构建 SwiftUI 应用) 平台 iOS 16+、iPadOS 16+、macOS 13+、watchOS 9+、tvOS 16+