axiom-swiftui-nav-ref

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

SwiftUI Navigation API Reference

SwiftUI导航API参考文档

Overview

概述

SwiftUI's navigation APIs provide data-driven, programmatic navigation that scales from simple stacks to complex multi-column layouts. Introduced in iOS 16 (2022) with NavigationStack and NavigationSplitView, evolved in iOS 18 (2024) with Tab/Sidebar unification, and refined in iOS 26 (2025) with Liquid Glass design.
SwiftUI的导航API提供了数据驱动的程序化导航,可从简单的栈式布局扩展到复杂的多列布局。iOS 16(2022年)推出了NavigationStack和NavigationSplitView,iOS 18(2024年)实现了Tab/侧边栏统一,iOS 26(2025年)则优化了Liquid Glass设计。

Evolution timeline

演进时间线

  • 2022 (iOS 16) NavigationStack, NavigationSplitView, NavigationPath, value-based NavigationLink
  • 2024 (iOS 18) Tab/Sidebar unification, sidebarAdaptable style, zoom navigation transition
  • 2025 (iOS 26) Liquid Glass navigation chrome, bottom-aligned search, floating tab bars, backgroundExtensionEffect
  • 2022(iOS 16) NavigationStack、NavigationSplitView、NavigationPath、基于值的NavigationLink
  • 2024(iOS 18) Tab/侧边栏统一、sidebarAdaptable样式、缩放导航过渡
  • 2025(iOS 26) Liquid Glass导航栏、底部对齐搜索、浮动标签栏、backgroundExtensionEffect

Key capabilities

核心功能

  • Data-driven navigation NavigationPath represents stack state, enabling programmatic push/pop and deep linking
  • Multi-column layouts NavigationSplitView adapts automatically (3-column on iPad → single stack on iPhone)
  • State restoration Codable NavigationPath + SceneStorage for persistence across app launches
  • Tab integration Per-tab NavigationStack with state preservation on tab switch (iOS 18+)
  • Liquid Glass Automatic glass navigation bars, sidebars, and toolbars (iOS 26+)
  • 数据驱动导航 NavigationPath代表栈状态,支持程序化推入/弹出和深度链接
  • 多列布局 NavigationSplitView自动适配(iPad上为3列 → iPhone上为单栈)
  • 状态恢复 可编码的NavigationPath + SceneStorage实现跨应用启动的持久化
  • Tab集成 每个Tab拥有独立的NavigationStack,切换Tab时保留状态(iOS 18+)
  • Liquid Glass 自动生成玻璃态导航栏、侧边栏和工具栏(iOS 26+)

When to use vs UIKit

SwiftUI导航与UIKit的选择场景

  • SwiftUI navigation New apps, multiplatform, simpler navigation flows → Use NavigationStack/SplitView
  • UINavigationController Complex coordinator patterns, legacy code, specific UIKit features → Consider UIKit
  • SwiftUI导航 新应用、多平台、简单导航流程 → 使用NavigationStack/SplitView
  • UINavigationController 复杂协调器模式、遗留代码、特定UIKit功能 → 考虑使用UIKit

Related Skills

相关技能

  • Use
    axiom-swiftui-nav
    for anti-patterns, decision trees, pressure scenarios
  • Use
    axiom-swiftui-nav-diag
    for systematic troubleshooting of navigation issues

  • 使用
    axiom-swiftui-nav
    了解反模式、决策树和压力场景
  • 使用
    axiom-swiftui-nav-diag
    进行导航问题的系统化排查

When to Use This Skill

何时使用本技能

Use this skill when:
  • Learning navigation APIs from NavigationStack to NavigationSplitView to NavigationPath
  • Implementing WWDC examples (all 4 sessions with code examples included)
  • Planning deep linking with URL routing and NavigationPath manipulation
  • Setting up state restoration with Codable NavigationPath and SceneStorage
  • Adopting iOS 26+ features Liquid Glass navigation, bottom-aligned search, tab bar minimization
  • Choosing navigation architecture Stack vs SplitView vs Tab+Navigation patterns
  • Implementing coordinator/router patterns alongside SwiftUI's built-in navigation

在以下场景中使用本技能:
  • 学习导航API 从NavigationStack到NavigationSplitView再到NavigationPath
  • 实现WWDC示例 包含所有4场会议的代码示例
  • 规划深度链接 涉及URL路由和NavigationPath操作
  • 设置状态恢复 使用可编码的NavigationPath和SceneStorage
  • 适配iOS 26+功能 Liquid Glass导航、底部对齐搜索、标签栏最小化
  • 选择导航架构 栈式、SplitView或Tab+导航模式
  • 实现协调器/路由模式 配合SwiftUI内置导航

API Evolution

API演进

Timeline

时间线

YeariOS VersionKey Features
2020iOS 14NavigationView (deprecated iOS 16)
2022iOS 16NavigationStack, NavigationSplitView, NavigationPath, value-based NavigationLink
2024iOS 18Tab/Sidebar unification, sidebarAdaptable, TabSection, zoom transitions
2025iOS 26Liquid Glass navigation, backgroundExtensionEffect, tabBarMinimizeBehavior
年份iOS版本核心功能
2020iOS 14NavigationView(iOS 16已废弃)
2022iOS 16NavigationStack、NavigationSplitView、NavigationPath、基于值的NavigationLink
2024iOS 18Tab/侧边栏统一、sidebarAdaptable、TabSection、缩放过渡
2025iOS 26Liquid Glass导航、backgroundExtensionEffect、tabBarMinimizeBehavior

NavigationView (Deprecated) vs NavigationStack/SplitView

NavigationView(已废弃)与NavigationStack/SplitView对比

FeatureNavigationView (iOS 13-15)NavigationStack/SplitView (iOS 16+)
Programmatic navigationPer-link
isActive
bindings
Single NavigationPath for entire stack
Deep linkingComplex, error-proneSimple path manipulation
Type safetyView-based, runtime checksValue-based, compile-time checks
State restorationManual, difficultBuilt-in Codable support
Multi-columnNavigationStyle enumDedicated NavigationSplitView
StatusDeprecated iOS 16Current API
功能NavigationView(iOS 13-15)NavigationStack/SplitView(iOS 16+)
程序化导航每个链接使用
isActive
绑定
单个NavigationPath管理整个栈
深度链接复杂且容易出错简单的路径操作
类型安全基于视图,运行时检查基于值,编译时检查
状态恢复手动实现,难度大内置可编码支持
多列布局使用NavigationStyle枚举专用的NavigationSplitView
状态iOS 16已废弃当前推荐API

Recommendation

建议

  • New apps: Use NavigationStack and NavigationSplitView exclusively
  • Existing apps: Migrate from NavigationView (deprecated)
  • See "Migrating to new navigation types" documentation

  • 新应用:仅使用NavigationStack和NavigationSplitView
  • 现有应用:从NavigationView迁移(已废弃)
  • 查看「迁移到新导航类型」文档

NavigationStack Complete Reference

NavigationStack完整参考

NavigationStack represents a push-pop interface like Settings on iPhone or System Settings on macOS.
NavigationStack代表类似iPhone「设置」或macOS「系统设置」的推入-弹出界面。

1.1 Creating NavigationStack

1.1 创建NavigationStack

Basic NavigationStack

基础NavigationStack

swift
NavigationStack {
    List(Category.allCases) { category in
        NavigationLink(category.name, value: category)
    }
    .navigationTitle("Categories")
    .navigationDestination(for: Category.self) { category in
        CategoryDetail(category: category)
    }
}
swift
NavigationStack {
    List(Category.allCases) { category in
        NavigationLink(category.name, value: category)
    }
    .navigationTitle("Categories")
    .navigationDestination(for: Category.self) { category in
        CategoryDetail(category: category)
    }
}

With Path Binding (WWDC 2022, 6:05)

带路径绑定(WWDC 2022,6:05)

swift
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)
    }
}
Key points:
  • path: $path
    binds the navigation state to a collection
  • Value-presenting
    NavigationLink
    appends values to the path
  • navigationDestination(for:)
    maps values to views
swift
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)
    }
}
关键点:
  • path: $path
    将导航状态绑定到集合
  • 基于值的
    NavigationLink
    会将值追加到路径中
  • navigationDestination(for:)
    将值映射到对应视图

1.2 NavigationLink (Value-Based)

1.2 NavigationLink(基于值)

Value-presenting NavigationLink

基于值的NavigationLink

swift
// 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
}
swift
// 正确:基于值(iOS 16+)
NavigationLink(recipe.name, value: recipe)

// 正确:自定义标签
NavigationLink(value: recipe) {
    RecipeTile(recipe: recipe)
}

// 已废弃:基于视图(iOS 13-15)
NavigationLink(recipe.name) {
    RecipeDetail(recipe: recipe)  // 新代码中请勿使用
}

How NavigationLink works with NavigationStack

NavigationLink与NavigationStack的协作方式

  1. NavigationStack maintains a
    path
    collection
  2. Tapping a value-presenting link appends the value to the path
  3. NavigationStack maps
    navigationDestination
    modifiers over path values
  4. Views are pushed onto the stack based on destination mappings
  1. NavigationStack维护一个
    path
    集合
  2. 点击基于值的链接会将值追加到路径中
  3. NavigationStack对路径中的值应用
    navigationDestination
    修饰符
  4. 根据目标映射将视图推入栈中

1.3 navigationDestination Modifier

1.3 navigationDestination修饰符

Single Type

单一类型

swift
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}
swift
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}

Multiple Types

多种类型

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

Placement rules

放置规则

  • Place
    navigationDestination
    outside lazy containers (not inside ForEach)
  • Place near related NavigationLinks for code organization
  • Must be inside NavigationStack hierarchy
swift
// 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)
        }
}
  • navigationDestination
    放在惰性容器外部(不要在ForEach内部)
  • 为了代码组织,放在相关NavigationLink附近
  • 必须在NavigationStack层级内
swift
// 正确:在惰性容器外部
ScrollView {
    LazyVGrid(columns: columns) {
        ForEach(recipes) { recipe in
            NavigationLink(value: recipe) {
                RecipeTile(recipe: recipe)
            }
        }
    }
}
.navigationDestination(for: Recipe.self) { recipe in
    RecipeDetail(recipe: recipe)
}

// 错误:在ForEach内部(可能无法加载)
ForEach(recipes) { recipe in
    NavigationLink(value: recipe) { RecipeTile(recipe: recipe) }
        .navigationDestination(for: Recipe.self) { r in  // 请勿这样做
            RecipeDetail(recipe: r)
        }
}

1.4 NavigationPath

1.4 NavigationPath

NavigationPath is a type-erased collection for heterogeneous navigation stacks.
NavigationPath是用于异构导航栈的类型擦除集合。

Typed Array vs NavigationPath

类型化数组与NavigationPath对比

swift
// Typed array: All values same type
@State private var path: [Recipe] = []

// NavigationPath: Mixed types
@State private var path = NavigationPath()
swift
// 类型化数组:所有值为同一类型
@State private var path: [Recipe] = []

// NavigationPath:支持混合类型
@State private var path = NavigationPath()

NavigationPath Operations

NavigationPath操作

swift
// 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)
swift
// 追加值
path.append(recipe)

// 返回到上一级
path.removeLast()

// 返回根视图
path.removeLast(path.count)
// 或者
path = NavigationPath()

// 检查数量
if path.count > 0 { ... }

// 深度链接:设置多个值
path.append(category)
path.append(recipe)

Codable Support

可编码支持

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

swift
// 当所有值都可编码时,NavigationPath支持Codable
@State private var path = NavigationPath()

// 编码
let data = try JSONEncoder().encode(path.codable)

// 解码
let codableRep = try JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data)
path = NavigationPath(codableRep)

NavigationSplitView Complete Reference

NavigationSplitView完整参考

NavigationSplitView creates multi-column layouts that adapt to device size.
NavigationSplitView创建可适配设备尺寸的多列布局。

2.1 Two-Column Layout

2.1 两列布局

Basic Two-Column (WWDC 2022, 10:40)

基础两列布局(WWDC 2022,10:40)

swift
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")
            }
        }
    }
}
swift
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("选择一个食谱")
            }
        }
    }
}

2.2 Three-Column Layout

2.2 三列布局

Three-Column with Content Column

带内容列的三列布局

swift
NavigationSplitView {
    // Sidebar
    List(Category.allCases, selection: $selectedCategory) { category in
        NavigationLink(category.localizedName, value: category)
    }
    .navigationTitle("Categories")
} content: {
    // Content column
    List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
        NavigationLink(recipe.name, value: recipe)
    }
    .navigationTitle(selectedCategory?.localizedName ?? "Recipes")
} detail: {
    // Detail column
    RecipeDetail(recipe: selectedRecipe)
}
swift
NavigationSplitView {
    // 侧边栏
    List(Category.allCases, selection: $selectedCategory) { category in
        NavigationLink(category.localizedName, value: category)
    }
    .navigationTitle("Categories")
} content: {
    // 内容列
    List(dataModel.recipes(in: selectedCategory), selection: $selectedRecipe) { recipe in
        NavigationLink(recipe.name, value: recipe)
    }
    .navigationTitle(selectedCategory?.localizedName ?? "Recipes")
} detail: {
    // 详情列
    RecipeDetail(recipe: selectedRecipe)
}

2.3 NavigationSplitView with NavigationStack (WWDC 2022, 14:10)

2.3 结合NavigationStack的NavigationSplitView(WWDC 2022,14:10)

Combine split view selection with stack-based drill-down:
swift
struct MultipleColumnsWithStack: View {
    @State private var selectedCategory: Category?
    @State private var path: [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: {
            NavigationStack(path: $path) {
                RecipeGrid(category: selectedCategory)
                    .navigationDestination(for: Recipe.self) { recipe in
                        RecipeDetail(recipe: recipe)
                    }
            }
        }
        .environmentObject(dataModel)
    }
}
Key pattern: NavigationStack inside NavigationSplitView detail column enables grid-to-detail drill-down while preserving sidebar selection.
将分栏视图选择与栈式钻取结合:
swift
struct MultipleColumnsWithStack: View {
    @State private var selectedCategory: Category?
    @State private var path: [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: {
            NavigationStack(path: $path) {
                RecipeGrid(category: selectedCategory)
                    .navigationDestination(for: Recipe.self) { recipe in
                        RecipeDetail(recipe: recipe)
                    }
            }
        }
        .environmentObject(dataModel)
    }
}
核心模式: 在NavigationSplitView的详情列中嵌入NavigationStack,实现从网格到详情的钻取,同时保留侧边栏选择状态。

2.4 Column Visibility

2.4 列可见性

swift
@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
swift
@State private var columnVisibility: NavigationSplitViewVisibility = .all

NavigationSplitView(columnVisibility: $columnVisibility) {
    Sidebar()
} content: {
    Content()
} detail: {
    Detail()
}

// 程序化控制可见性
columnVisibility = .detailOnly  // 隐藏侧边栏和内容列
columnVisibility = .all          // 显示所有列
columnVisibility = .automatic    // 由系统决定

2.5 Automatic Adaptation

2.5 自动适配

NavigationSplitView automatically adapts:
  • iPad landscape All columns visible (depending on configuration)
  • iPad portrait/Slide Over Collapses to overlay or single column
  • iPhone Single navigation stack
  • Apple Watch/TV Single navigation stack
Selection changes automatically translate to push/pop on iPhone.
NavigationSplitView会自动适配:
  • iPad横屏 显示所有列(取决于配置)
  • iPad竖屏/侧拉 折叠为覆盖层或单列
  • iPhone 单导航栈
  • Apple Watch/TV 单导航栈
在iPhone上,选择变化会自动转换为推入/弹出操作。

2.6 iOS 26+ Liquid Glass Sidebar (WWDC 2025, 323)

2.6 iOS 26+ Liquid Glass侧边栏(WWDC 2025,323)

swift
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

swift
NavigationSplitView {
    List { ... }
} detail: {
    DetailView()
}
// 在iPad/macOS上,侧边栏自动获得Liquid Glass外观

// 让内容延伸到侧边栏后方
.backgroundExtensionEffect()  // 镜像并模糊安全区域外的内容

Deep Linking and URL Routing

深度链接与URL路由

3.1 Basic Deep Link Handling

3.1 基础深度链接处理

swift
struct ContentView: View {
    @State private var path = NavigationPath()

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

    func handleDeepLink(_ url: URL) {
        // Parse URL: myapp://recipe/apple-pie
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let host = components.host else { return }

        switch host {
        case "recipe":
            if let recipeName = components.path.dropFirst().description,
               let recipe = dataModel.recipe(named: recipeName) {
                path.removeLast(path.count)  // Pop to root
                path.append(recipe)           // Push recipe
            }
        case "category":
            if let categoryName = components.path.dropFirst().description,
               let category = Category(rawValue: categoryName) {
                path.removeLast(path.count)
                path.append(category)
            }
        default:
            break
        }
    }
}
swift
struct ContentView: View {
    @State private var path = NavigationPath()

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

    func handleDeepLink(_ url: URL) {
        // 解析URL: myapp://recipe/apple-pie
        guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
              let host = components.host else { return }

        switch host {
        case "recipe":
            if let recipeName = components.path.dropFirst().description,
               let recipe = dataModel.recipe(named: recipeName) {
                path.removeLast(path.count)  // 返回根视图
                path.append(recipe)           // 推入食谱视图
            }
        case "category":
            if let categoryName = components.path.dropFirst().description,
               let category = Category(rawValue: categoryName) {
                path.removeLast(path.count)
                path.append(category)
            }
        default:
            break
        }
    }
}

3.2 Multi-Step Deep Links

3.2 多步骤深度链接

swift
// URL: myapp://category/desserts/recipe/apple-pie
func handleDeepLink(_ url: URL) {
    let pathComponents = url.pathComponents.filter { $0 != "/" }

    path.removeLast(path.count)  // Reset to root

    var index = 0
    while index < pathComponents.count {
        let component = pathComponents[index]

        switch component {
        case "category":
            if index + 1 < pathComponents.count,
               let category = Category(rawValue: pathComponents[index + 1]) {
                path.append(category)
                index += 2
            }
        case "recipe":
            if index + 1 < pathComponents.count,
               let recipe = dataModel.recipe(named: pathComponents[index + 1]) {
                path.append(recipe)
                index += 2
            }
        default:
            index += 1
        }
    }
}

swift
// URL: myapp://category/desserts/recipe/apple-pie
func handleDeepLink(_ url: URL) {
    let pathComponents = url.pathComponents.filter { $0 != "/" }

    path.removeLast(path.count)  // 重置到根视图

    var index = 0
    while index < pathComponents.count {
        let component = pathComponents[index]

        switch component {
        case "category":
            if index + 1 < pathComponents.count,
               let category = Category(rawValue: pathComponents[index + 1]) {
                path.append(category)
                index += 2
            }
        case "recipe":
            if index + 1 < pathComponents.count,
               let recipe = dataModel.recipe(named: pathComponents[index + 1]) {
                path.append(recipe)
                index += 2
            }
        default:
            index += 1
        }
    }
}

State Restoration

状态恢复

4.1 Complete State Restoration (WWDC 2022, 18:12)

4.1 完整状态恢复(WWDC 2022,18:12)

swift
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)
    }
}
swift
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 {
            // 出现时恢复状态
            if let data = data {
                navModel.jsonData = data
            }
            // 变化时保存状态
            for await _ in navModel.objectWillChangeSequence {
                data = navModel.jsonData
            }
        }
        .environmentObject(dataModel)
    }
}

4.2 Codable NavigationModel

4.2 可编码的NavigationModel

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

        // Convert IDs back to objects, discarding deleted items
        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
    }

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

    var objectWillChangeSequence: AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>> {
        objectWillChange
            .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest)
            .values
    }
}
Key pattern: Store IDs, not full model objects. Use
compactMap
to handle deleted items gracefully.

swift
class NavigationModel: ObservableObject, Codable {
    @Published var selectedCategory: Category?
    @Published var recipePath: [Recipe] = []

    enum CodingKeys: String, CodingKey {
        case selectedCategory
        case recipePathIds  // 存储ID,而非完整对象
    }

    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)

        // 将ID转换回对象,优雅处理已删除的项
        let recipePathIds = try container.decode([Recipe.ID].self, forKey: .recipePathIds)
        self.recipePath = recipePathIds.compactMap { DataModel.shared[$0] }
    }

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

    var objectWillChangeSequence: AsyncPublisher<Publishers.Buffer<ObservableObjectPublisher>> {
        objectWillChange
            .buffer(size: 1, prefetch: .byRequest, whenFull: .dropOldest)
            .values
    }
}
核心模式: 存储ID而非完整模型对象。使用
compactMap
优雅处理已删除的项。

Tab + Navigation Integration

Tab + 导航集成

5.1 Tab Syntax (iOS 18+) (WWDC 2024, 4:27)

5.1 Tab语法(iOS 18+)(WWDC 2024,4:27)

swift
TabView {
    Tab("Watch Now", systemImage: "play") {
        WatchNowView()
    }
    Tab("Library", systemImage: "books.vertical") {
        LibraryView()
    }
    Tab(role: .search) {
        NavigationStack {
            SearchView()
                .navigationTitle("Search")
        }
        .searchable(text: $searchText)
    }
}
Search tab requirement: Contents of a search-role tab must be wrapped in
NavigationStack
with
.searchable()
applied to the stack. Without
NavigationStack
, the search field will not appear. For foundational
.searchable
patterns (suggestions, scopes, tokens, programmatic control), see
axiom-swiftui-search-ref
.
swift
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 TabView with NavigationStack Per Tab

5.2 每个Tab独立使用NavigationStack

swift
TabView {
    Tab("Home", systemImage: "house") {
        NavigationStack {
            HomeView()
                .navigationDestination(for: Item.self) { item in
                    ItemDetail(item: item)
                }
        }
    }
    Tab("Settings", systemImage: "gear") {
        NavigationStack {
            SettingsView()
        }
    }
}
Key pattern: Each tab has its own NavigationStack to preserve navigation state when switching tabs.
swift
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 Sidebar-Adaptable TabView (WWDC 2024, 6:41)

5.3 可适配侧边栏的TabView(WWDC 2024,6:41)

swift
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)
Key features:
  • TabSection
    creates groups visible in sidebar
  • .sidebarAdaptable
    enables sidebar on iPad, tab bar on iPhone
  • Search tab with
    .search
    role gets special placement
swift
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") {
        // 更多Tabs...
    }
    Tab(role: .search) {
        SearchView()
    }
}
.tabViewStyle(.sidebarAdaptable)
核心功能:
  • TabSection
    创建在侧边栏中可见的分组
  • .sidebarAdaptable
    在iPad上显示侧边栏,在iPhone上显示标签栏
  • .search
    角色的搜索Tab获得特殊布局

5.4 Tab Customization (WWDC 2024, 10:45)

5.4 Tab自定义(WWDC 2024,10:45)

swift
@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)
swift
@AppStorage("MyTabViewCustomization")
private var customization: TabViewCustomization

TabView {
    Tab("Watch Now", systemImage: "play", value: .watchNow) {
        WatchNowView()
    }
    .customizationID("Tab.watchNow")
    .customizationBehavior(.disabled, for: .sidebar, .tabBar)  // 无法隐藏

    Tab("Optional Tab", systemImage: "star", value: .optional) {
        OptionalView()
    }
    .customizationID("Tab.optional")
    .defaultVisibility(.hidden, for: .tabBar)  // 默认隐藏
}
.tabViewCustomization($customization)

5.5 Programmatic Tab Visibility

5.5 程序化控制Tab可见性

Use
.hidden(_:)
to show/hide tabs based on app state while preserving their navigation state.
使用
.hidden(_:)
根据应用状态显示/隐藏Tab,同时保留其导航状态。

State-Driven Tab Visibility

状态驱动的Tab可见性

swift
enum AppContext { case home, browse }

struct ContentView: View {
    @State private var context: AppContext = .home
    @State private var selection: TabID = .home

    var body: some View {
        TabView(selection: $selection) {
            Tab("Home", systemImage: "house") {
                HomeView()
            }
            .tag(TabID.home)

            Tab("Libraries", systemImage: "square.stack") {
                LibrariesView()
            }
            .tag(TabID.libraries)
            .hidden(context == .browse)  // Hide in browse context

            Tab("Playlists", systemImage: "music.note.list") {
                PlaylistsView()
            }
            .tag(TabID.playlists)
            .hidden(context == .browse)

            Tab("Tracks", systemImage: "music.note") {
                TracksView()
            }
            .tag(TabID.tracks)
            .hidden(context == .home)    // Hide in home context
        }
        .tabViewStyle(.sidebarAdaptable)
    }
}
swift
enum AppContext { case home, browse }

struct ContentView: View {
    @State private var context: AppContext = .home
    @State private var selection: TabID = .home

    var body: some View {
        TabView(selection: $selection) {
            Tab("Home", systemImage: "house") {
                HomeView()
            }
            .tag(TabID.home)

            Tab("Libraries", systemImage: "square.stack") {
                LibrariesView()
            }
            .tag(TabID.libraries)
            .hidden(context == .browse)  // 在浏览上下文隐藏

            Tab("Playlists", systemImage: "music.note.list") {
                PlaylistsView()
            }
            .tag(TabID.playlists)
            .hidden(context == .browse)

            Tab("Tracks", systemImage: "music.note") {
                TracksView()
            }
            .tag(TabID.tracks)
            .hidden(context == .home)    // 在主页上下文隐藏
        }
        .tabViewStyle(.sidebarAdaptable)
    }
}

State Preservation

状态保留

Key difference:
.hidden(_:)
preserves tab state, conditional rendering does not.
swift
// ✅ 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
    }
}
关键区别
.hidden(_:)
保留Tab状态,条件渲染则不保留。
swift
// ✅ 隐藏时保留状态
Tab("Settings", systemImage: "gear") {
    SettingsView()  // 导航栈被保留
}
.hidden(!showSettings)

// ❌ 条件变化时丢失状态
if showSettings {
    Tab("Settings", systemImage: "gear") {
        SettingsView()  // 导航栈被重新创建
    }
}

Common Patterns

常见模式

Feature Flags
swift
Tab("Beta Features", systemImage: "flask") {
    BetaView()
}
.hidden(!UserDefaults.standard.bool(forKey: "enableBetaFeatures"))
Authentication State
swift
Tab("Profile", systemImage: "person.circle") {
    ProfileView()
}
.hidden(!authManager.isAuthenticated)
Purchase Status
swift
Tab("Pro Features", systemImage: "star.circle.fill") {
    ProFeaturesView()
}
.hidden(!purchaseManager.isPro)
Development Builds
swift
Tab("Debug", systemImage: "hammer") {
    DebugView()
}
.hidden(!isDevelopmentBuild)

private var isDevelopmentBuild: Bool {
    #if DEBUG
    return true
    #else
    return false
    #endif
}
功能开关
swift
Tab("Beta Features", systemImage: "flask") {
    BetaView()
}
.hidden(!UserDefaults.standard.bool(forKey: "enableBetaFeatures"))
认证状态
swift
Tab("Profile", systemImage: "person.circle") {
    ProfileView()
}
.hidden(!authManager.isAuthenticated)
购买状态
swift
Tab("Pro Features", systemImage: "star.circle.fill") {
    ProFeaturesView()
}
.hidden(!purchaseManager.isPro)
开发构建
swift
Tab("Debug", systemImage: "hammer") {
    DebugView()
}
.hidden(!isDevelopmentBuild)

private var isDevelopmentBuild: Bool {
    #if DEBUG
    return true
    #else
    return false
    #endif
}

Animated Transitions

动画过渡

Wrap state changes in
withAnimation
for smooth tab bar layout transitions:
swift
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
使用
withAnimation
包裹状态变化,实现平滑的标签栏布局过渡:
swift
Button("切换到浏览模式") {
    withAnimation {
        context = .browse
        selection = .tracks  // 切换到第一个可见的Tab
    }
}
// 标签栏会随着Tab的显示/隐藏进行动画
// 自动使用系统运动曲线

5.6 iOS 26+ Tab Features (WWDC 2025, 256)

5.6 iOS 26+ Tab功能(WWDC 2025,256)

swift
// 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
swift
// 滚动时标签栏最小化
TabView { ... }
    .tabBarMinimizeBehavior(.onScrollDown)

// 底部辅助视图(始终可见)
TabView { ... }
    .tabViewBottomAccessory {
        PlaybackControls()
    }

// 动态可见性(推荐用于迷你播放器)
// ⚠️ 需要iOS 26.1+(非26.0)
TabView { ... }
    .tabViewBottomAccessory(isEnabled: showMiniPlayer) {
        MiniPlayerView()
            .transition(.opacity)
    }
// isEnabled: true = 显示辅助视图
// isEnabled: false = 隐藏并移除预留空间

// 带专用搜索字段的搜索Tab
Tab(role: .search) {
    NavigationStack {
        SearchView()
            .navigationTitle("Search")
    }
    .searchable(text: $searchText)
}
// 选中时会变形为搜索字段
// ⚠️ 必须包裹NavigationStack才能显示搜索字段
// 回退方案:如果没有Tab使用.search角色,TabView会将搜索应用到所有Tab,切换Tab时重置搜索状态

Dynamic Bottom Accessory

动态底部辅助视图

The accessory view can change based on the active tab, though Apple's own usage (Music mini-player) keeps it global:
swift
@State private var activeTab: TabID = .workouts

TabView(selection: $activeTab) { /* tabs */ }
    .tabViewBottomAccessory {
        switch activeTab {
        case .workouts:
            Button("Start Workout") { }
        case .exercises:
            Button("Add Exercise") { }
        default:
            EmptyView()
        }
    }
Accessory placement: On iPhone, the bottom accessory position depends on tab bar state. When the tab bar is normal size, the accessory appears above it; when the tab bar is collapsed (via
tabBarMinimizeBehavior
), the accessory displays inline. Read the
tabViewBottomAccessoryPlacement
environment value to adjust content:
swift
struct AdaptiveAccessory: View {
    @Environment(\.tabViewBottomAccessoryPlacement) var placement

    var body: some View {
        HStack {
            NowPlayingInfo()
            if placement == .bar {
                // Full controls when above tab bar
                PlaybackControls()
            } else {
                // Compact when inline with collapsed tab bar
                PlayPauseButton()
            }
        }
    }
}
Best practice: Reserve
tabViewBottomAccessory
for content relevant across all tabs (playback controls, status indicators). For tab-specific actions, prefer floating glass buttons within the tab's content view.
辅助视图可根据当前激活的Tab变化,不过苹果自身的用法(音乐迷你播放器)将其设为全局:
swift
@State private var activeTab: TabID = .workouts

TabView(selection: $activeTab) { /* tabs */ }
    .tabViewBottomAccessory {
        switch activeTab {
        case .workouts:
            Button("开始训练") { }
        case .exercises:
            Button("添加训练动作") { }
        default:
            EmptyView()
        }
    }
辅助视图布局:在iPhone上,底部辅助视图的位置取决于标签栏状态。当标签栏为正常尺寸时,辅助视图显示在其上方;当标签栏通过
tabBarMinimizeBehavior
折叠时,辅助视图内联显示。读取
tabViewBottomAccessoryPlacement
环境值来调整内容:
swift
struct AdaptiveAccessory: View {
    @Environment(\.tabViewBottomAccessoryPlacement) var placement

    var body: some View {
        HStack {
            NowPlayingInfo()
            if placement == .bar {
                // 在标签栏上方时显示完整控件
                PlaybackControls()
            } else {
                // 与折叠标签栏内联时显示紧凑控件
                PlayPauseButton()
            }
        }
    }
}
最佳实践
tabViewBottomAccessory
保留给所有Tab都相关的内容(播放控件、状态指示器)。对于Tab特定的操作,优先在Tab的内容视图中使用浮动玻璃态按钮。

5.7 Tab API Quick Reference

5.7 Tab API快速参考

ModifierTargetiOSPurpose
Tab(_:systemImage:value:content:)
18+New tab syntax with selection value
Tab(role: .search)
18+Semantic search tab with morph behavior
TabSection(_:content:)
18+Group tabs in sidebar view
.customizationID(_:)
Tab18+Enable user customization
.customizationBehavior(_:for:)
Tab18+Control hide/reorder permissions
.defaultVisibility(_:for:)
Tab18+Set initial visibility state
.hidden(_:)
Tab18+Programmatic visibility with state preservation
.tabViewStyle(.sidebarAdaptable)
TabView18+Sidebar on iPad, tabs on iPhone
.tabViewCustomization($binding)
TabView18+Persist user tab arrangement
.tabBarMinimizeBehavior(_:)
TabView26+Auto-hide on scroll
.tabViewBottomAccessory(isEnabled:content:)
TabView26.1+Dynamic content below tab bar

修饰符目标iOS用途
Tab(_:systemImage:value:content:)
18+带选择值的新Tab语法
Tab(role: .search)
18+具有变形行为的语义化搜索Tab
TabSection(_:content:)
18+在侧边栏视图中分组Tabs
.customizationID(_:)
Tab18+启用用户自定义
.customizationBehavior(_:for:)
Tab18+控制隐藏/重排权限
.defaultVisibility(_:for:)
Tab18+设置初始可见性状态
.hidden(_:)
Tab18+程序化控制可见性并保留状态
.tabViewStyle(.sidebarAdaptable)
TabView18+iPad显示侧边栏,iPhone显示标签栏
.tabViewCustomization($binding)
TabView18+持久化用户的Tab布局
.tabBarMinimizeBehavior(_:)
TabView26+滚动时自动隐藏
.tabViewBottomAccessory(isEnabled:content:)
TabView26.1+标签栏下方的动态内容

iOS 26+ Navigation Features

iOS 26+导航功能

6.1 Liquid Glass Navigation (WWDC 2025, 323)

6.1 Liquid Glass导航(WWDC 2025,323)

Automatic adoption when building with Xcode 26:
  • Navigation bars become Liquid Glass
  • Sidebars float above content with glass effect
  • Tab bars float with new compact appearance
  • Toolbars get automatic grouping
使用Xcode 26构建时自动适配:
  • 导航栏变为Liquid Glass样式
  • 侧边栏悬浮在内容上方,带有玻璃效果
  • 标签栏以新的紧凑外观悬浮
  • 工具栏自动分组

6.2 Background Extension Effect

6.2 背景扩展效果

swift
NavigationSplitView {
    Sidebar()
} detail: {
    HeroImage()
        .backgroundExtensionEffect()  // Content extends behind sidebar
}
swift
NavigationSplitView {
    Sidebar()
} detail: {
    HeroImage()
        .backgroundExtensionEffect()  // 内容延伸到侧边栏后方
}

6.3 Bottom-Aligned Search (WWDC 2025, 256)

6.3 底部对齐搜索(WWDC 2025,256)

Foundational search APIs For
.searchable
,
isSearching
, suggestions, scopes, tokens, and programmatic control, see
axiom-swiftui-search-ref
. This section covers iOS 26 bottom-aligned refinement only.
swift
NavigationSplitView {
    Sidebar()
} detail: {
    DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad
基础搜索API 关于
.searchable
isSearching
、建议、范围、令牌和程序化控制,请查看
axiom-swiftui-search-ref
。本节仅涵盖iOS 26的底部对齐优化。
swift
NavigationSplitView {
    Sidebar()
} detail: {
    DetailView()
}
.searchable(text: $query, prompt: "你在找什么?")
// 在iPhone上自动底部对齐,在iPad上自动右上对齐

6.4 Scroll Edge Effect

6.4 滚动边缘效果

swift
// 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
swift
// 内容滚动到工具栏下方时自动应用模糊效果
// 移除所有自定义暗化背景 - 它们会干扰效果

// 对于密集UI,调整锐度
ScrollView { ... }
    .scrollEdgeEffectStyle(.soft)  // .sharp, .soft

6.5 Tab Bar Minimization

6.5 标签栏最小化

swift
TabView {
    Tab("Home", systemImage: "house") {
        NavigationStack {
            ScrollView {
                // Content
            }
        }
    }
}
.tabBarMinimizeBehavior(.onScrollDown)  // Minimizes on scroll
swift
TabView {
    Tab("Home", systemImage: "house") {
        NavigationStack {
            ScrollView {
                // 内容
            }
        }
    }
}
.tabBarMinimizeBehavior(.onScrollDown)  // 滚动时最小化

6.6 Sheet Presentations with Zoom Transition

6.6 带缩放过渡的Sheet展示

In iOS 26, sheets can morph directly out of the buttons that present them. Make the presenting toolbar item a source for a navigation zoom transition, and mark the sheet content as the destination:
swift
@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))
}
Other presentations also flow smoothly out of Liquid Glass controls — menus, alerts, and popovers. Dialogs automatically morph out of the buttons that present them without additional code.
Audit tip: If you've used
presentationBackground
to apply custom backgrounds to sheets, consider removing it and let the new Liquid Glass sheet material shine. Partial height sheets are now inset with glass background by default.
在iOS 26中,Sheet可以直接从触发它的按钮变形展开。将触发的工具栏项标记为导航缩放过渡的源,并将Sheet内容标记为目标:
swift
@Namespace private var namespace

// Sheet从触发按钮变形展开
.toolbar {
    ToolbarItem {
        Button("设置") { showSettings = true }
            .matchedTransitionSource(id: "settings", in: namespace)
    }
}
.sheet(isPresented: $showSettings) {
    SettingsView()
        .navigationTransition(.zoom(sourceID: "settings", in: namespace))
}
其他展示方式(菜单、提醒、弹出框)也能从Liquid Glass控件平滑展开。对话框无需额外代码,会自动从触发按钮变形展开。
审核提示:如果您使用
presentationBackground
为Sheet应用了自定义背景,考虑移除它,让新的Liquid Glass Sheet材质发挥效果。部分高度的Sheet现在默认使用玻璃背景并带有内边距。

6.7 Toolbar Morphing Transitions

6.7 工具栏变形过渡

iOS 26 automatically morphs toolbars during NavigationStack push/pop when each destination view declares its own
.toolbar {}
. Items with matching
toolbar(id:)
and
ToolbarItem(id:)
IDs stay stable during the transition (no bounce), while unmatched items animate in/out.
Key rule: Attach
.toolbar {}
to individual views inside NavigationStack, not to NavigationStack itself. Otherwise there is nothing to morph between.
See
axiom-swiftui-26-ref
skill for complete toolbar morphing API including DefaultToolbarItem,
toolbar(id:)
stable items, ToolbarSpacer patterns, and troubleshooting.

在iOS 26中,当每个目标视图都声明了自己的
.toolbar {}
时,NavigationStack推入/弹出期间工具栏会自动变形。具有匹配
toolbar(id:)
ToolbarItem(id:)
ID的项在过渡期间保持稳定(无弹跳),不匹配的项则会动画进出。
核心规则:将
.toolbar {}
附加到NavigationStack内的各个视图,而非NavigationStack本身。否则没有可变形的对象。
查看
axiom-swiftui-26-ref
技能获取完整的工具栏变形API,包括DefaultToolbarItem、
toolbar(id:)
稳定项、ToolbarSpacer模式和故障排除。

Router/Coordinator Patterns

路由/协调器模式

7.1 When to Use Coordinators

7.1 何时使用协调器

Use coordinators when:
  • Navigation logic is complex with conditional flows
  • Testing navigation in isolation
  • Sharing navigation logic across multiple screens
  • UIKit interop with heavy navigation requirements
Use built-in navigation when:
  • Simple linear or hierarchical navigation
  • State restoration is primary concern
  • Fewer than 5-10 navigation destinations
  • No need for navigation unit testing
在以下场景使用协调器:
  • 导航逻辑复杂,包含条件流程
  • 独立测试导航
  • 在多个屏幕间共享导航逻辑
  • 与UIKit交互且有大量导航需求
在以下场景使用内置导航:
  • 简单的线性或层级导航
  • 状态恢复是主要关注点
  • 导航目标少于5-10个
  • 无需对导航进行单元测试

7.2 Simple Router Pattern

7.2 简单路由模式

swift
// 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))
        }
    }
}
swift
// Route枚举定义所有可能的目标
enum AppRoute: Hashable {
    case home
    case category(Category)
    case recipe(Recipe)
    case settings
}

// Router类管理导航
@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()
        }
    }
}

// 在视图中使用
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)
    }
}

// 在子视图中使用
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 Pattern with Protocol

7.3 带协议的协调器模式

swift
protocol Coordinator {
    associatedtype Route: Hashable
    var path: NavigationPath { get set }
    func navigate(to route: Route)
}

@Observable
class RecipeCoordinator: Coordinator {
    typealias Route = RecipeRoute
    var path = NavigationPath()

    enum RecipeRoute: Hashable {
        case list(Category)
        case detail(Recipe)
        case edit(Recipe)
        case relatedRecipes(Recipe)
    }

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

    func showRecipeOfTheDay() {
        path.removeLast(path.count)
        if let recipe = DataModel.shared.recipeOfTheDay {
            path.append(RecipeRoute.detail(recipe))
        }
    }
}
swift
protocol Coordinator {
    associatedtype Route: Hashable
    var path: NavigationPath { get set }
    func navigate(to route: Route)
}

@Observable
class RecipeCoordinator: Coordinator {
    typealias Route = RecipeRoute
    var path = NavigationPath()

    enum RecipeRoute: Hashable {
        case list(Category)
        case detail(Recipe)
        case edit(Recipe)
        case relatedRecipes(Recipe)
    }

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

    func showRecipeOfTheDay() {
        path.removeLast(path.count)
        if let recipe = DataModel.shared.recipeOfTheDay {
            path.append(RecipeRoute.detail(recipe))
        }
    }
}

7.4 Testing Navigation

7.4 测试导航

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

swift
// Router易于测试
func testNavigateToRecipe() {
    let router = Router()
    let recipe = Recipe(name: "苹果派")

    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: "苹果派")))

    router.popToRoot()

    XCTAssertTrue(router.path.isEmpty)
}

Testing Checklist

测试检查清单

Navigation Flow Testing

导航流程测试

  • All NavigationLinks navigate to correct destination
  • Back button returns to previous view
  • Pop to root clears entire stack
  • Deep links navigate correctly from cold start
  • Deep links navigate correctly when app is running
  • 所有NavigationLink导航到正确的目标
  • 返回按钮回到上一级视图
  • 返回根视图清空整个栈
  • 冷启动时深度链接导航正确
  • 应用运行时深度链接导航正确

State Restoration Testing

状态恢复测试

  • Navigation state persists when app backgrounds
  • Navigation state restores on app launch
  • Deleted items handled gracefully (compactMap)
  • SceneStorage key is unique per scene
  • 导航状态在应用后台时持久化
  • 应用启动时恢复导航状态
  • 已删除项被优雅处理(compactMap)
  • SceneStorage键在每个场景中唯一

Multi-Platform Testing

多平台测试

  • NavigationSplitView collapses correctly on iPhone
  • Selection in sidebar pushes on iPhone
  • Tab bar visible and functional on all platforms
  • Sidebar toggle works on iPad
  • NavigationSplitView在iPhone上正确折叠
  • 侧边栏中的选择在iPhone上触发推入
  • 标签栏在所有平台上可见且可用
  • iPad上的侧边栏切换正常工作

iOS 26+ Testing

iOS 26+测试

  • Liquid Glass appearance correct
  • Bottom-aligned search on iPhone
  • Tab bar minimization works
  • Scroll edge effect not interfering with custom backgrounds

  • Liquid Glass外观正确
  • iPhone上的底部对齐搜索正常
  • 标签栏最小化功能正常
  • 滚动边缘效果未干扰自定义背景

API Quick Reference

API快速参考

NavigationStack

NavigationStack

swift
NavigationStack { content }
NavigationStack(path: $path) { content }
swift
NavigationStack { content }
NavigationStack(path: $path) { content }

NavigationSplitView

NavigationSplitView

swift
NavigationSplitView { sidebar } detail: { detail }
NavigationSplitView { sidebar } content: { content } detail: { detail }
NavigationSplitView(columnVisibility: $visibility) { ... }
swift
NavigationSplitView { sidebar } detail: { detail }
NavigationSplitView { sidebar } content: { content } detail: { detail }
NavigationSplitView(columnVisibility: $visibility) { ... }

NavigationLink

NavigationLink

swift
NavigationLink(title, value: value)
NavigationLink(value: value) { label }
swift
NavigationLink(title, value: value)
NavigationLink(value: value) { label }

NavigationPath

NavigationPath

swift
path.append(value)
path.removeLast()
path.removeLast(path.count)
path.count
path.codable  // For encoding
NavigationPath(codableRepresentation)  // For decoding
swift
path.append(value)
path.removeLast()
path.removeLast(path.count)
path.count
path.codable  // 用于编码
NavigationPath(codableRepresentation)  // 用于解码

Modifiers

修饰符

swift
.navigationTitle("Title")
.navigationDestination(for: Type.self) { value in View }
.searchable(text: $query)
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.backgroundExtensionEffect()

swift
.navigationTitle("标题")
.navigationDestination(for: Type.self) { value in 视图 }
.searchable(text: $query)
.tabViewStyle(.sidebarAdaptable)
.tabBarMinimizeBehavior(.onScrollDown)
.backgroundExtensionEffect()

Resources

资源

WWDC: 2022-10054, 2024-10147, 2025-256, 2025-323 (Build a SwiftUI app with the new design)
Docs: /swiftui/tabrole/search, /swiftui/view/tabbarminimizebehavior(_:), /swiftui/view/tabviewbottomaccessory(isenabled:content:)
Skills: axiom-swiftui-nav, axiom-swiftui-nav-diag, axiom-swiftui-26-ref, axiom-liquid-glass, axiom-swiftui-search-ref

Last Updated Based on WWDC 2022-10054, WWDC 2024-10147, WWDC 2025-256, WWDC 2025-323 (Build a SwiftUI app with the new design) Platforms iOS 16+, iPadOS 16+, macOS 13+, watchOS 9+, tvOS 16+
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+