mcpField
A
axiom-swiftui-nav-ref

axiom-swiftui-nav-ref

github.com/charleswiltgen开发1.2k198 次安装今天更新
代码仓库

关于 axiom-swiftui-nav-ref

面向 SwiftUI 开发者的导航 API 参考指南,介绍 NavigationStack、NavigationSplitView、NavigationPath 与 Tab 集成。还涵盖深层链接、状态恢复和 API 演进,帮助实现从单列到多列的导航结构。

主要功能

  1. 01NavigationStack 与 NavigationPath 用法
  2. 02自适应多列导航布局
  3. 03深层链接与 URL 路由
  4. 04导航状态持久化与恢复
  5. 05Tab 与导航栈集成

适用场景

  1. 01实现类型安全的程序化页面跳转
  2. 02为 iPhone 和 iPad 设计自适应导航
  3. 03根据 URL 打开应用内指定页面
  4. 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 版本主要功能
2020iOS 14NavigationView(iOS 16 起已弃用)
2022iOS 16NavigationStack、NavigationSplitView、NavigationPath、基于值的 NavigationLink
2024iOS 18Tab/Sidebar 统一、sidebarAdaptable、TabSection、缩放过渡
2025iOS 26Liquid 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 配合
  1. NavigationStack 维护一个 path 集合
  2. 点击呈现值的链接会将该值追加到路径中
  3. NavigationStack 会将 navigationDestination 修饰符映射到路径中的值
  4. 视图会根据目标映射推入堆栈
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:)Tab18+为标签的侧边栏表示形式添加上下文菜单
.customizationID(_:)Tab18+启用用户自定义
.customizationBehavior(_:for:)Tab18+控制隐藏/重新排序权限
.defaultVisibility(_:for:)Tab18+设置初始可见状态
.hidden(_:)Tab18+以编程方式控制可见性并保留状态
.tabViewStyle(.sidebarAdaptable)TabView18+iPad 上显示侧边栏,iPhone 上显示标签
.tabViewCustomization($binding)TabView18+持久化用户的标签排列
.tabBarMinimizeBehavior(_:)TabView26+滚动时自动隐藏
.tabViewBottomAccessory(isEnabled:content:)TabView26.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+

评论

支持 Markdown