axiom-swiftui-nav-ref
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI 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 for anti-patterns, decision trees, pressure scenarios
axiom-swiftui-nav - Use for systematic troubleshooting of navigation issues
axiom-swiftui-nav-diag
- 使用了解反模式、决策树和压力场景
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
时间线
| Year | iOS Version | Key Features |
|---|---|---|
| 2020 | iOS 14 | NavigationView (deprecated iOS 16) |
| 2022 | iOS 16 | NavigationStack, NavigationSplitView, NavigationPath, value-based NavigationLink |
| 2024 | iOS 18 | Tab/Sidebar unification, sidebarAdaptable, TabSection, zoom transitions |
| 2025 | iOS 26 | Liquid Glass navigation, backgroundExtensionEffect, tabBarMinimizeBehavior |
| 年份 | iOS版本 | 核心功能 |
|---|---|---|
| 2020 | iOS 14 | NavigationView(iOS 16已废弃) |
| 2022 | iOS 16 | NavigationStack、NavigationSplitView、NavigationPath、基于值的NavigationLink |
| 2024 | iOS 18 | Tab/侧边栏统一、sidebarAdaptable、TabSection、缩放过渡 |
| 2025 | iOS 26 | Liquid Glass导航、backgroundExtensionEffect、tabBarMinimizeBehavior |
NavigationView (Deprecated) vs NavigationStack/SplitView
NavigationView(已废弃)与NavigationStack/SplitView对比
| Feature | NavigationView (iOS 13-15) | NavigationStack/SplitView (iOS 16+) |
|---|---|---|
| Programmatic navigation | Per-link | Single NavigationPath for entire stack |
| Deep linking | Complex, error-prone | Simple path manipulation |
| Type safety | View-based, runtime checks | Value-based, compile-time checks |
| State restoration | Manual, difficult | Built-in Codable support |
| Multi-column | NavigationStyle enum | Dedicated NavigationSplitView |
| Status | Deprecated iOS 16 | Current API |
| 功能 | NavigationView(iOS 13-15) | NavigationStack/SplitView(iOS 16+) |
|---|---|---|
| 程序化导航 | 每个链接使用 | 单个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:
- binds the navigation state to a collection
path: $path - Value-presenting appends values to the path
NavigationLink - maps values to views
navigationDestination(for:)
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的协作方式
- NavigationStack maintains a collection
path - Tapping a value-presenting link appends the value to the path
- NavigationStack maps modifiers over path values
navigationDestination - Views are pushed onto the stack based on destination mappings
- NavigationStack维护一个集合
path - 点击基于值的链接会将值追加到路径中
- NavigationStack对路径中的值应用修饰符
navigationDestination - 根据目标映射将视图推入栈中
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 outside lazy containers (not inside ForEach)
navigationDestination - 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)
}
}- 将放在惰性容器外部(不要在ForEach内部)
navigationDestination - 为了代码组织,放在相关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 decidesswift
@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 areaswift
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 to handle deleted items gracefully.
compactMapswift
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而非完整模型对象。使用优雅处理已删除的项。
compactMapTab + 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 with applied to the stack. Without , the search field will not appear. For foundational patterns (suggestions, scopes, tokens, programmatic control), see .
NavigationStack.searchable()NavigationStack.searchableaxiom-swiftui-search-refswift
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.searchableaxiom-swiftui-search-ref5.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:
- creates groups visible in sidebar
TabSection - enables sidebar on iPad, tab bar on iPhone
.sidebarAdaptable - Search tab with role gets special placement
.search
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 - 在iPad上显示侧边栏,在iPhone上显示标签栏
.sidebarAdaptable - 带角色的搜索Tab获得特殊布局
.search
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 to show/hide tabs based on app state while preserving their navigation state.
.hidden(_:)使用根据应用状态显示/隐藏Tab,同时保留其导航状态。
.hidden(_:)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: preserves tab state, conditional rendering does not.
.hidden(_:)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
}
}关键区别:保留Tab状态,条件渲染则不保留。
.hidden(_:)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 for smooth tab bar layout transitions:
withAnimationswift
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使用包裹状态变化,实现平滑的标签栏布局过渡:
withAnimationswift
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 changesswift
// 滚动时标签栏最小化
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 ), the accessory displays inline. Read the environment value to adjust content:
tabBarMinimizeBehaviortabViewBottomAccessoryPlacementswift
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 for content relevant across all tabs (playback controls, status indicators). For tab-specific actions, prefer floating glass buttons within the tab's content view.
tabViewBottomAccessory辅助视图可根据当前激活的Tab变化,不过苹果自身的用法(音乐迷你播放器)将其设为全局:
swift
@State private var activeTab: TabID = .workouts
TabView(selection: $activeTab) { /* tabs */ }
.tabViewBottomAccessory {
switch activeTab {
case .workouts:
Button("开始训练") { }
case .exercises:
Button("添加训练动作") { }
default:
EmptyView()
}
}辅助视图布局:在iPhone上,底部辅助视图的位置取决于标签栏状态。当标签栏为正常尺寸时,辅助视图显示在其上方;当标签栏通过折叠时,辅助视图内联显示。读取环境值来调整内容:
tabBarMinimizeBehaviortabViewBottomAccessoryPlacementswift
struct AdaptiveAccessory: View {
@Environment(\.tabViewBottomAccessoryPlacement) var placement
var body: some View {
HStack {
NowPlayingInfo()
if placement == .bar {
// 在标签栏上方时显示完整控件
PlaybackControls()
} else {
// 与折叠标签栏内联时显示紧凑控件
PlayPauseButton()
}
}
}
}最佳实践:保留给所有Tab都相关的内容(播放控件、状态指示器)。对于Tab特定的操作,优先在Tab的内容视图中使用浮动玻璃态按钮。
tabViewBottomAccessory5.7 Tab API Quick Reference
5.7 Tab API快速参考
| Modifier | Target | iOS | Purpose |
|---|---|---|---|
| — | 18+ | New tab syntax with selection value |
| — | 18+ | Semantic search tab with morph behavior |
| — | 18+ | Group tabs in sidebar view |
| Tab | 18+ | Enable user customization |
| Tab | 18+ | Control hide/reorder permissions |
| Tab | 18+ | Set initial visibility state |
| Tab | 18+ | Programmatic visibility with state preservation |
| TabView | 18+ | Sidebar on iPad, tabs on iPhone |
| TabView | 18+ | Persist user tab arrangement |
| TabView | 26+ | Auto-hide on scroll |
| TabView | 26.1+ | Dynamic content below tab bar |
| 修饰符 | 目标 | iOS | 用途 |
|---|---|---|---|
| — | 18+ | 带选择值的新Tab语法 |
| — | 18+ | 具有变形行为的语义化搜索Tab |
| — | 18+ | 在侧边栏视图中分组Tabs |
| Tab | 18+ | 启用用户自定义 |
| Tab | 18+ | 控制隐藏/重排权限 |
| Tab | 18+ | 设置初始可见性状态 |
| Tab | 18+ | 程序化控制可见性并保留状态 |
| TabView | 18+ | iPad显示侧边栏,iPhone显示标签栏 |
| TabView | 18+ | 持久化用户的Tab布局 |
| TabView | 26+ | 滚动时自动隐藏 |
| TabView | 26.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 , , suggestions, scopes, tokens, and programmatic control, see . This section covers iOS 26 bottom-aligned refinement only.
.searchableisSearchingaxiom-swiftui-search-refswift
NavigationSplitView {
Sidebar()
} detail: {
DetailView()
}
.searchable(text: $query, prompt: "What are you looking for?")
// Automatically bottom-aligned on iPhone, top-trailing on iPad基础搜索API 关于、、建议、范围、令牌和程序化控制,请查看。本节仅涵盖iOS 26的底部对齐优化。
.searchableisSearchingaxiom-swiftui-search-refswift
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, .softswift
// 内容滚动到工具栏下方时自动应用模糊效果
// 移除所有自定义暗化背景 - 它们会干扰效果
// 对于密集UI,调整锐度
ScrollView { ... }
.scrollEdgeEffectStyle(.soft) // .sharp, .soft6.5 Tab Bar Minimization
6.5 标签栏最小化
swift
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
ScrollView {
// Content
}
}
}
}
.tabBarMinimizeBehavior(.onScrollDown) // Minimizes on scrollswift
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 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.
presentationBackground在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控件平滑展开。对话框无需额外代码,会自动从触发按钮变形展开。
审核提示:如果您使用为Sheet应用了自定义背景,考虑移除它,让新的Liquid Glass Sheet材质发挥效果。部分高度的Sheet现在默认使用玻璃背景并带有内边距。
presentationBackground6.7 Toolbar Morphing Transitions
6.7 工具栏变形过渡
iOS 26 automatically morphs toolbars during NavigationStack push/pop when each destination view declares its own . Items with matching and IDs stay stable during the transition (no bounce), while unmatched items animate in/out.
.toolbar {}toolbar(id:)ToolbarItem(id:)Key rule: Attach to individual views inside NavigationStack, not to NavigationStack itself. Otherwise there is nothing to morph between.
.toolbar {}See skill for complete toolbar morphing API including DefaultToolbarItem, stable items, ToolbarSpacer patterns, and troubleshooting.
axiom-swiftui-26-reftoolbar(id:)在iOS 26中,当每个目标视图都声明了自己的时,NavigationStack推入/弹出期间工具栏会自动变形。具有匹配和ID的项在过渡期间保持稳定(无弹跳),不匹配的项则会动画进出。
.toolbar {}toolbar(id:)ToolbarItem(id:)核心规则:将附加到NavigationStack内的各个视图,而非NavigationStack本身。否则没有可变形的对象。
.toolbar {}查看技能获取完整的工具栏变形API,包括DefaultToolbarItem、稳定项、ToolbarSpacer模式和故障排除。
axiom-swiftui-26-reftoolbar(id:)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 decodingswift
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+