axiom-swiftui-nav
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseSwiftUI Navigation
SwiftUI 导航
When to Use This Skill
何时使用此技能
Use when:
- Choosing navigation architecture (NavigationStack vs NavigationSplitView vs TabView)
- Implementing programmatic navigation with NavigationPath
- Setting up deep linking and URL routing
- Implementing state restoration for navigation
- Adopting Tab/Sidebar patterns (iOS 18+)
- Implementing coordinator/router patterns
- Requesting code review of navigation implementation before shipping
适用于以下场景:
- 选择导航架构(NavigationStack vs NavigationSplitView vs TabView)
- 使用NavigationPath实现程序化导航
- 设置深度链接和URL路由
- 实现导航的状态恢复
- 采用标签栏/侧边栏模式(iOS 18+)
- 实现协调器/路由器模式
- 上线前请求对导航实现进行代码评审
Related Skills
相关技能
- Use for systematic troubleshooting of navigation failures
axiom-swiftui-nav-diag - Use for comprehensive API reference (including Tab customization, iOS 26+ features) with all WWDC examples
axiom-swiftui-nav-ref
- 使用对导航失败问题进行系统性排查
axiom-swiftui-nav-diag - 使用获取完整的API参考(包括标签栏自定义、iOS 26+功能)以及所有WWDC示例
axiom-swiftui-nav-ref
Example Prompts
示例提示
These are real questions developers ask that this skill is designed to answer:
以下是开发者实际会提出的、本技能可解答的问题:
1. "Should I use NavigationStack or NavigationSplitView for my app?"
1. "我应该为我的应用使用NavigationStack还是NavigationSplitView?"
-> The skill provides a decision tree based on device targets, content hierarchy depth, and multiplatform requirements
-> 本技能会根据设备目标、内容层级深度和多平台要求提供决策树
2. "How do I navigate programmatically in SwiftUI?"
2. "如何在SwiftUI中实现程序化导航?"
-> The skill shows NavigationPath manipulation patterns for push, pop, pop-to-root, and deep linking
-> 本技能展示用于推送、弹出、返回根视图和深度链接的NavigationPath操作模式
3. "My deep links aren't working. The app opens but shows the wrong screen."
3. "我的深度链接无法正常工作。应用能打开,但显示错误的页面。"
-> The skill covers URL parsing patterns, path construction order, and timing issues with onOpenURL
-> 本技能涵盖URL解析模式、路径构建顺序以及onOpenURL的时序问题
4. "Navigation state is lost when my app goes to background."
4. "应用进入后台后导航状态丢失。"
-> The skill demonstrates Codable NavigationPath, SceneStorage persistence, and crash-resistant restoration
-> 本技能演示可编码的NavigationPath、SceneStorage持久化以及防崩溃的恢复方案
5. "How do I implement a coordinator pattern in SwiftUI?"
5. "如何在SwiftUI中实现协调器模式?"
-> The skill provides Router pattern examples alongside guidance on when coordinators add value vs complexity
-> 本技能提供路由器模式示例,同时指导何时使用协调器能带来价值,何时会增加复杂度
Red Flags — Anti-Patterns to Prevent
警示 — 需避免的反模式
If you're doing ANY of these, STOP and use the patterns in this skill:
如果你正在做以下任何一件事,请立即停止并使用本技能中的模式:
❌ CRITICAL — Never Do These
❌ 严重错误 — 绝对不要这样做
1. Using deprecated NavigationView on iOS 16+
1. 在iOS 16+中使用已弃用的NavigationView
swift
// ❌ WRONG — Deprecated, different behavior on iOS 16+
NavigationView {
List { ... }
}
.navigationViewStyle(.stack)Why this fails NavigationView is deprecated since iOS 16. It lacks NavigationPath support, making programmatic navigation and deep linking unreliable. Different behavior across iOS versions causes bugs.
swift
// ❌ 错误 — 已弃用,在iOS 16+上行为不同
NavigationView {
List { ... }
}
.navigationViewStyle(.stack)失败原因 NavigationView自iOS 16起已被弃用。它不支持NavigationPath,导致程序化导航和深度链接不可靠。不同iOS版本间的行为差异会引发Bug。
2. Using view-based NavigationLink for programmatic navigation
2. 使用基于视图的NavigationLink实现程序化导航
swift
// ❌ WRONG — Cannot programmatically control
NavigationLink("Recipe") {
RecipeDetail(recipe: recipe) // View destination, no value
}Why this fails View-based links cannot be controlled programmatically. No way to deep link or pop to this destination. Deprecated since iOS 16.
swift
// ❌ 错误 — 无法进行程序化控制
NavigationLink("Recipe") {
RecipeDetail(recipe: recipe) // 视图目标,无值绑定
}失败原因 基于视图的链接无法被程序化控制。无法实现深度链接或返回到该目标页面。自iOS 16起已被弃用。
3. Putting navigationDestination inside lazy containers
3. 在惰性容器内放置navigationDestination
swift
// ❌ WRONG — May not be loaded when needed
LazyVGrid(columns: columns) {
ForEach(items) { item in
NavigationLink(value: item) { ... }
.navigationDestination(for: Item.self) { item in // Don't do this
ItemDetail(item: item)
}
}
}Why this fails Lazy containers don't load all views immediately. navigationDestination may not be visible to NavigationStack, causing navigation to silently fail.
swift
// ❌ 错误 — 可能在需要时未加载
LazyVGrid(columns: columns) {
ForEach(items) { item in
NavigationLink(value: item) { ... }
.navigationDestination(for: Item.self) { item in // 不要这样做
ItemDetail(item: item)
}
}
}失败原因 惰性容器不会立即加载所有视图。navigationDestination可能无法被NavigationStack识别,导致导航静默失败。
4. Storing full model objects in NavigationPath for restoration
4. 在NavigationPath中存储完整模型对象用于状态恢复
swift
// ❌ WRONG — Duplicates data, stale on restore
class NavigationModel: Codable {
var path: [Recipe] = [] // Full Recipe objects
}Why this fails Duplicates data already in your model. On restore, Recipe data may be stale (edited/deleted elsewhere). Use IDs and resolve to current data.
swift
// ❌ 错误 — 数据重复,恢复时可能过期
class NavigationModel: Codable {
var path: [Recipe] = [] // 完整的Recipe对象
}失败原因 会重复已存在于模型中的数据。恢复时,Recipe数据可能已过期(在其他地方被编辑/删除)。应使用ID并在恢复时解析为当前数据。
5. Modifying NavigationPath outside MainActor
5. 在MainActor外部修改NavigationPath
swift
// ❌ WRONG — UI update off main thread
Task.detached {
await viewModel.path.append(recipe) // Background thread
}Why this fails NavigationPath binds to UI. Modifications must happen on MainActor or navigation state becomes corrupted. Can cause crashes or silent failures.
swift
// ❌ 错误 — 在后台线程更新UI
Task.detached {
await viewModel.path.append(recipe) // 后台线程
}失败原因 NavigationPath与UI绑定。修改操作必须在MainActor中执行,否则导航状态会损坏。可能导致崩溃或静默失败。
6. Missing @MainActor isolation for navigation state
6. 导航状态未使用@MainActor隔离
swift
// ❌ WRONG — Not MainActor isolated
class Router: ObservableObject {
@Published var path = NavigationPath() // No @MainActor
}Why this fails In Swift 6 strict concurrency, @Published properties accessed from SwiftUI views require MainActor isolation. Causes data race warnings and potential crashes.
swift
// ❌ 错误 — 未使用MainActor隔离
class Router: ObservableObject {
@Published var path = NavigationPath() // 无@MainActor
}失败原因 在Swift 6严格并发模式下,SwiftUI视图访问的@Published属性需要MainActor隔离。会导致数据竞争警告和潜在崩溃。
7. Not handling navigation state in multi-tab apps
7. 未在多标签应用中处理导航状态
swift
// ❌ WRONG — Shared NavigationPath across tabs
TabView {
Tab("Home") { HomeView() }
Tab("Settings") { SettingsView() }
}
// All tabs share same NavigationStack — wrong!Why this fails Each tab should have its own NavigationStack to preserve navigation state when switching tabs. Shared state causes confusing UX.
swift
// ❌ 错误 — 所有标签共享同一个NavigationPath
TabView {
Tab("Home") { HomeView() }
Tab("Settings") { SettingsView() }
}
// 所有标签共享同一个NavigationStack — 错误!失败原因 每个标签应拥有自己的NavigationStack以在切换标签时保留导航状态。共享状态会导致混乱的用户体验。
8. Ignoring NavigationPath decoding errors
8. 忽略NavigationPath解码错误
swift
// ❌ WRONG — Crashes on invalid data
let path = NavigationPath(try! decoder.decode(NavigationPath.CodableRepresentation.self, from: data))Why this fails User may have deleted items that were in the path. Schema may have changed. Force unwrap causes crash on restore.
swift
// ❌ 错误 — 无效数据会导致崩溃
let path = NavigationPath(try! decoder.decode(NavigationPath.CodableRepresentation.self, from: data))失败原因 用户可能已删除路径中包含的项目。架构可能已更改。强制解包会导致恢复时崩溃。
Mandatory First Steps
强制前置步骤
ALWAYS complete these steps before implementing navigation:
swift
// Step 1: Identify your navigation structure
// Ask: Single stack? Multi-column? Tab-based with per-tab navigation?
// Record answer before writing any code
// Step 2: Choose container based on structure
// Single stack (iPhone-primary): NavigationStack
// Multi-column (iPad/Mac-primary): NavigationSplitView
// Tab-based: TabView with NavigationStack per tab
// Step 3: Define your value types for navigation
// All values pushed on NavigationStack must be Hashable
// For deep linking/restoration, also Codable
struct Recipe: Hashable, Codable, Identifiable { ... }
// Step 4: Plan deep link URLs (if needed)
// myapp://recipe/{id}
// myapp://category/{name}/recipe/{id}
// Step 5: Plan state restoration (if needed)
// Will you use SceneStorage? What data must be Codable?在实现导航前必须完成以下步骤:
swift
// 步骤1:确定你的导航结构
// 思考:单栈?多列?每个标签带独立导航的标签式?
// 在编写任何代码前记录答案
// 步骤2:根据结构选择容器
// 单栈(以iPhone为主):NavigationStack
// 多列(以iPad/Mac为主):NavigationSplitView
// 标签式:TabView + 每个标签配NavigationStack
// 步骤3:定义导航使用的值类型
// 所有推入NavigationStack的值必须是Hashable
// 如需深度链接/状态恢复,还需是Codable
struct Recipe: Hashable, Codable, Identifiable { ... }
// 步骤4:规划深度链接URL(如有需要)
// myapp://recipe/{id}
// myapp://category/{name}/recipe/{id}
// 步骤5:规划状态恢复(如有需要)
// 是否使用SceneStorage?哪些数据需要是Codable?Quick Decision Tree
快速决策树
Need navigation?
├─ Multi-column interface (iPad/Mac primary)?
│ └─ NavigationSplitView
│ ├─ Need drill-down in detail column?
│ │ └─ NavigationStack inside detail (Pattern 3)
│ └─ Selection-only detail?
│ └─ Just selection binding (Pattern 2)
├─ Tab-based app?
│ └─ TabView
│ ├─ Each tab needs drill-down?
│ │ └─ NavigationStack per tab (Pattern 4)
│ └─ iPad sidebar experience?
│ └─ .tabViewStyle(.sidebarAdaptable) (Pattern 5)
└─ Single-column stack?
└─ NavigationStack
├─ Need deep linking?
│ └─ Use NavigationPath (Pattern 1b)
└─ Simple push/pop?
└─ Typed array path (Pattern 1a)
Need state restoration?
└─ SceneStorage + Codable NavigationPath (Pattern 6)
Need coordinator abstraction?
├─ Complex conditional flows?
├─ Navigation logic testing needed?
├─ Sharing navigation across many screens?
└─ YES to any → Router pattern (Pattern 7)
NO to all → Use NavigationPath directly需要导航?
├─ 多列界面(以iPad/Mac为主)?
│ └─ NavigationSplitView
│ ├─ 详情列需要逐级深入?
│ │ └─ 在详情列内使用NavigationStack(模式3)
│ └─ 仅选择式详情?
│ └─ 仅使用选择绑定(模式2)
├─ 标签式应用?
│ └─ TabView
│ ├─ 每个标签需要逐级深入?
│ │ └─ 每个标签配NavigationStack(模式4)
│ └─ iPad侧边栏体验?
│ └─ .tabViewStyle(.sidebarAdaptable)(模式5)
└─ 单列栈?
└─ NavigationStack
├─ 需要深度链接?
│ └─ 使用NavigationPath(模式1b)
└─ 简单的推送/弹出?
└─ 类型化数组路径(模式1a)
需要状态恢复?
└─ SceneStorage + 可编码的NavigationPath(模式6)
需要协调器抽象?
├─ 复杂的条件流程?
├─ 需要测试导航逻辑?
├─ 多个页面共享导航?
└─ 以上任意为是 → 路由器模式(模式7)
全部为否 → 直接使用NavigationPathPattern 1a: Basic NavigationStack
模式1a:基础NavigationStack
When: Simple push/pop navigation, all destinations same type
Time cost: 5-10 min
swift
struct RecipeList: View {
@State private var path: [Recipe] = []
var body: some View {
NavigationStack(path: $path) {
List(recipes) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle("Recipes")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
}
// Programmatic navigation
func showRecipe(_ recipe: Recipe) {
path.append(recipe)
}
func popToRoot() {
path.removeAll()
}
}Key points:
- Typed array when all values are same type
[Recipe] - Value-based
NavigationLink(title, value:) - outside lazy containers
navigationDestination(for:)
适用场景:简单的推送/弹出导航,所有目标页面类型相同
时间成本:5-10分钟
swift
struct RecipeList: View {
@State private var path: [Recipe] = []
var body: some View {
NavigationStack(path: $path) {
List(recipes) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle("Recipes")
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
}
// 程序化导航
func showRecipe(_ recipe: Recipe) {
path.append(recipe)
}
func popToRoot() {
path.removeAll()
}
}关键点:
- 所有值类型相同时使用类型化数组
[Recipe] - 使用基于值的
NavigationLink(title, value:) - 放置在惰性容器外部
navigationDestination(for:)
Pattern 1b: NavigationStack with Deep Linking
模式1b:带深度链接的NavigationStack
When: Multiple destination types, URL-based deep linking
Time cost: 15-20 min
swift
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: Category.self) { category in
CategoryView(category: category)
}
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
.onOpenURL { url in
handleDeepLink(url)
}
}
func handleDeepLink(_ url: URL) {
// URL: myapp://category/desserts/recipe/apple-pie
path.removeLast(path.count) // Pop to root first
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
let segments = components.path.split(separator: "/").map(String.init)
var index = 0
while index < segments.count - 1 {
switch segments[index] {
case "category":
if let category = Category(rawValue: segments[index + 1]) {
path.append(category)
}
index += 2
case "recipe":
if let recipe = dataModel.recipe(named: segments[index + 1]) {
path.append(recipe)
}
index += 2
default:
index += 1
}
}
}
}Key points:
- for heterogeneous types
NavigationPath - Pop to root before building deep link path
- Build path in correct order (parent → child)
适用场景:多种目标页面类型,基于URL的深度链接
时间成本:15-20分钟
swift
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) {
HomeView()
.navigationDestination(for: Category.self) { category in
CategoryView(category: category)
}
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
}
.onOpenURL { url in
handleDeepLink(url)
}
}
func handleDeepLink(_ url: URL) {
// URL: myapp://category/desserts/recipe/apple-pie
path.removeLast(path.count) // 先返回根视图
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return }
let segments = components.path.split(separator: "/").map(String.init)
var index = 0
while index < segments.count - 1 {
switch segments[index] {
case "category":
if let category = Category(rawValue: segments[index + 1]) {
path.append(category)
}
index += 2
case "recipe":
if let recipe = dataModel.recipe(named: segments[index + 1]) {
path.append(recipe)
}
index += 2
default:
index += 1
}
}
}
}关键点:
- 异构类型使用
NavigationPath - 构建深度链接路径前先返回根视图
- 按正确顺序构建路径(父视图 → 子视图)
Pattern 2: NavigationSplitView Selection-Based
模式2:基于选择的NavigationSplitView
When: Multi-column layout where detail shows selected item
Time cost: 10-15 min
swift
struct MultiColumnView: View {
@State private var selectedCategory: Category?
@State private var selectedRecipe: Recipe?
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categories")
} content: {
if let category = selectedCategory {
List(recipes(in: category), selection: $selectedRecipe) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle(category.name)
} else {
Text("Select a category")
}
} detail: {
if let recipe = selectedRecipe {
RecipeDetail(recipe: recipe)
} else {
Text("Select a recipe")
}
}
}
}Key points:
- on List connects to column selection
selection: $binding - Value-presenting links update selection automatically
- Adapts to single stack on iPhone
适用场景:多列布局,详情页显示选中的项目
时间成本:10-15分钟
swift
struct MultiColumnView: View {
@State private var selectedCategory: Category?
@State private var selectedRecipe: Recipe?
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categories")
} content: {
if let category = selectedCategory {
List(recipes(in: category), selection: $selectedRecipe) { recipe in
NavigationLink(recipe.name, value: recipe)
}
.navigationTitle(category.name)
} else {
Text("Select a category")
}
} detail: {
if let recipe = selectedRecipe {
RecipeDetail(recipe: recipe)
} else {
Text("Select a recipe")
}
}
}
}关键点:
- List上的关联列选择
selection: $binding - 基于值的链接会自动更新选择状态
- 在iPhone上自适应为单栈布局
Pattern 3: NavigationSplitView with Stack in Detail
模式3:详情列内带栈的NavigationSplitView
When: Multi-column with drill-down capability in detail
Time cost: 20-25 min
swift
struct GridWithDrillDown: View {
@State private var selectedCategory: Category?
@State private var path: [Recipe] = []
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categories")
} detail: {
NavigationStack(path: $path) {
if let category = selectedCategory {
RecipeGrid(category: category)
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
} else {
Text("Select a category")
}
}
}
}
}Key points:
- NavigationStack inside detail column
- Grid → Detail drill-down while preserving sidebar
- Separate path for drill-down, selection for sidebar
适用场景:多列布局,详情页需要逐级深入
时间成本:20-25分钟
swift
struct GridWithDrillDown: View {
@State private var selectedCategory: Category?
@State private var path: [Recipe] = []
var body: some View {
NavigationSplitView {
List(Category.allCases, selection: $selectedCategory) { category in
NavigationLink(category.name, value: category)
}
.navigationTitle("Categories")
} detail: {
NavigationStack(path: $path) {
if let category = selectedCategory {
RecipeGrid(category: category)
.navigationDestination(for: Recipe.self) { recipe in
RecipeDetail(recipe: recipe)
}
} else {
Text("Select a category")
}
}
}
}
}关键点:
- 在详情列内嵌入NavigationStack
- 网格 → 详情的逐级深入,同时保留侧边栏
- 逐级深入使用独立路径,侧边栏使用选择绑定
Pattern 4: TabView with Per-Tab NavigationStack
模式4:每个标签带NavigationStack的TabView
When: Tab-based app where each tab has its own navigation
Time cost: 15-20 min
swift
struct TabBasedApp: View {
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
HomeView()
.navigationDestination(for: Item.self) { item in
ItemDetail(item: item)
}
}
}
Tab("Search", systemImage: "magnifyingglass") {
NavigationStack {
SearchView()
}
}
Tab("Settings", systemImage: "gear") {
NavigationStack {
SettingsView()
}
}
}
}
}Key points:
- Each Tab has its own NavigationStack
- Navigation state preserved when switching tabs
- iOS 18+ Tab syntax with systemImage
适用场景:标签式应用,每个标签拥有独立导航
时间成本:15-20分钟
swift
struct TabBasedApp: View {
var body: some View {
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
HomeView()
.navigationDestination(for: Item.self) { item in
ItemDetail(item: item)
}
}
}
Tab("Search", systemImage: "magnifyingglass") {
NavigationStack {
SearchView()
}
}
Tab("Settings", systemImage: "gear") {
NavigationStack {
SettingsView()
}
}
}
}
}关键点:
- 每个Tab拥有独立的NavigationStack
- 切换标签时保留导航状态
- iOS 18+带systemImage的Tab语法
Pattern 5: Sidebar-Adaptable TabView (iOS 18+)
模式5:适配侧边栏的TabView(iOS 18+)
When: Tab bar on iPhone, sidebar on iPad
Time cost: 20-25 min
swift
struct AdaptableApp: View {
var body: some View {
TabView {
Tab("Watch Now", systemImage: "play") {
WatchNowView()
}
Tab("Library", systemImage: "books.vertical") {
LibraryView()
}
TabSection("Collections") {
Tab("Favorites", systemImage: "star") {
FavoritesView()
}
Tab("Recently Added", systemImage: "clock") {
RecentView()
}
}
Tab(role: .search) {
SearchView()
}
}
.tabViewStyle(.sidebarAdaptable)
}
}Key points:
- enables sidebar on iPad
.tabViewStyle(.sidebarAdaptable) - creates collapsible groups in sidebar
TabSection - gets special placement
Tab(role: .search)
适用场景:iPhone显示标签栏,iPad显示侧边栏
时间成本:20-25分钟
swift
struct AdaptableApp: View {
var body: some View {
TabView {
Tab("Watch Now", systemImage: "play") {
WatchNowView()
}
Tab("Library", systemImage: "books.vertical") {
LibraryView()
}
TabSection("Collections") {
Tab("Favorites", systemImage: "star") {
FavoritesView()
}
Tab("Recently Added", systemImage: "clock") {
RecentView()
}
}
Tab(role: .search) {
SearchView()
}
}
.tabViewStyle(.sidebarAdaptable)
}
}关键点:
- 在iPad上启用侧边栏
.tabViewStyle(.sidebarAdaptable) - 在侧边栏中创建可折叠分组
TabSection - 会获得特殊布局位置
Tab(role: .search)
Pattern 6: State Restoration
模式6:状态恢复
When: Preserve navigation state across app launches
Time cost: 25-30 min
swift
@MainActor
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe.ID] = [] // Store IDs, not objects
enum CodingKeys: String, CodingKey {
case selectedCategory, recipePath
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
try container.encode(recipePath, forKey: .recipePath)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
recipePath = try container.decode([Recipe.ID].self, forKey: .recipePath)
}
init() {}
var jsonData: Data? {
get { try? JSONEncoder().encode(self) }
set {
guard let data = newValue,
let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
else { return }
selectedCategory = model.selectedCategory
recipePath = model.recipePath
}
}
}
struct ContentView: View {
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationStack(path: $navModel.recipePath) {
// Content
}
.task {
if let data { navModel.jsonData = data }
for await _ in navModel.objectWillChange.values {
data = navModel.jsonData
}
}
}
}Key points:
- Store IDs, resolve to current objects
- for Swift 6 concurrency safety
@MainActor - SceneStorage for automatic scene-scoped persistence
- Use when resolving IDs to handle deleted items
compactMap
适用场景:跨应用启动保留导航状态
时间成本:25-30分钟
swift
@MainActor
class NavigationModel: ObservableObject, Codable {
@Published var selectedCategory: Category?
@Published var recipePath: [Recipe.ID] = [] // 存储ID而非对象
enum CodingKeys: String, CodingKey {
case selectedCategory, recipePath
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(selectedCategory, forKey: .selectedCategory)
try container.encode(recipePath, forKey: .recipePath)
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
selectedCategory = try container.decodeIfPresent(Category.self, forKey: .selectedCategory)
recipePath = try container.decode([Recipe.ID].self, forKey: .recipePath)
}
init() {}
var jsonData: Data? {
get { try? JSONEncoder().encode(self) }
set {
guard let data = newValue,
let model = try? JSONDecoder().decode(NavigationModel.self, from: data)
else { return }
selectedCategory = model.selectedCategory
recipePath = model.recipePath
}
}
}
struct ContentView: View {
@StateObject private var navModel = NavigationModel()
@SceneStorage("navigation") private var data: Data?
var body: some View {
NavigationStack(path: $navModel.recipePath) {
// 内容
}
.task {
if let data { navModel.jsonData = data }
for await _ in navModel.objectWillChange.values {
data = navModel.jsonData
}
}
}
}关键点:
- 存储ID,恢复时解析为当前对象
- 使用确保Swift 6并发安全
@MainActor - SceneStorage用于自动的场景级持久化
- 解析ID时使用处理已删除的项目
compactMap
Pattern 7: Router/Coordinator
模式7:路由器/协调器
When: Complex navigation logic, need testability
Time cost: 30-45 min
swift
enum AppRoute: Hashable {
case home
case category(Category)
case recipe(Recipe)
case settings
}
@Observable
@MainActor
class Router {
var path = NavigationPath()
func navigate(to route: AppRoute) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path.removeLast(path.count)
}
func showRecipeOfTheDay() {
popToRoot()
if let recipe = DataModel.shared.recipeOfTheDay {
path.append(AppRoute.recipe(recipe))
}
}
}
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 cat): CategoryView(category: cat)
case .recipe(let recipe): RecipeDetail(recipe: recipe)
case .settings: SettingsView()
}
}
}
.environment(router)
}
}When coordinators add value:
- Complex conditional navigation flows
- Navigation logic needs unit testing
- Multiple views trigger same navigation
- UIKit interop with custom transitions
When coordinators add complexity without value:
- Simple linear navigation
- < 5 navigation destinations
- No need for navigation testing
- NavigationPath already handles your deep linking
适用场景:复杂的导航逻辑,需要可测试性
时间成本:30-45分钟
swift
enum AppRoute: Hashable {
case home
case category(Category)
case recipe(Recipe)
case settings
}
@Observable
@MainActor
class Router {
var path = NavigationPath()
func navigate(to route: AppRoute) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path.removeLast(path.count)
}
func showRecipeOfTheDay() {
popToRoot()
if let recipe = DataModel.shared.recipeOfTheDay {
path.append(AppRoute.recipe(recipe))
}
}
}
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 cat): CategoryView(category: cat)
case .recipe(let recipe): RecipeDetail(recipe: recipe)
case .settings: SettingsView()
}
}
}
.environment(router)
}
}协调器带来价值的场景:
- 复杂的条件导航流程
- 导航逻辑需要单元测试
- 多个视图触发相同的导航操作
- 与UIKit互操作并使用自定义转场
协调器增加无价值复杂度的场景:
- 简单的线性导航
- 导航目标页面少于5个
- 无需测试导航逻辑
- NavigationPath已能满足深度链接需求
Anti-Patterns (DO NOT DO THIS)
反模式(绝对不要这样做)
❌ Nesting NavigationStack inside NavigationStack
❌ 在NavigationStack内嵌套NavigationStack
swift
// ❌ WRONG — Nested stacks
NavigationStack {
SomeView()
.sheet(isPresented: $showSheet) {
NavigationStack { // Creates separate stack — confusing
SheetContent()
}
}
}Issue Two navigation stacks create confusing UX. Back button behavior unclear.
Fix Use single NavigationStack, present sheets without nested navigation when possible.
swift
// ❌ 错误 — 嵌套栈
NavigationStack {
SomeView()
.sheet(isPresented: $showSheet) {
NavigationStack { // 创建独立栈 — 体验混乱
SheetContent()
}
}
}问题 两个导航栈会造成混乱的用户体验。返回按钮的行为不明确。
修复方案 使用单个NavigationStack,尽可能在不嵌套导航的情况下展示弹窗。
❌ Using NavigationLink inside Button
❌ 在Button内使用NavigationLink
swift
// ❌ WRONG — Double navigation triggers
Button("Go") {
// Some action
} label: {
NavigationLink(value: item) { // Fires on button AND link
Text("Item")
}
}Issue Both Button and NavigationLink respond to taps.
Fix Use only NavigationLink, put action in if needed.
.simultaneousGestureswift
// ❌ 错误 — 触发双重导航
Button("Go") {
// 某些操作
} label: {
NavigationLink(value: item) { // 按钮和链接都会响应点击
Text("Item")
}
}问题 Button和NavigationLink都会响应点击。
修复方案 仅使用NavigationLink,如需操作可放在中。
.simultaneousGesture❌ Creating NavigationPath in view body
❌ 在视图body中创建NavigationPath
swift
// ❌ WRONG — Recreated every render
var body: some View {
let path = NavigationPath() // Reset on every render!
NavigationStack(path: .constant(path)) { ... }
}Issue Path recreated each render, navigation state lost.
Fix Use or for navigation state.
@State@StateObjectswift
// ❌ 错误 — 每次渲染都会重建
var body: some View {
let path = NavigationPath() // 每次渲染都会重置!
NavigationStack(path: .constant(path)) { ... }
}问题 每次渲染都会重建路径,导致导航状态丢失。
修复方案 使用或存储导航状态。
@State@StateObjectPressure Scenario: "Make Navigation Like Instagram"
压力场景:"实现类似Instagram的导航"
The Problem
问题
Product/design asks for complex navigation like Instagram:
- "Tab bar with per-tab navigation stacks"
- "Smooth coordinator pattern for all flows"
- "Deep linking to any screen"
- "Profile accessible from anywhere"
产品/设计要求实现类似Instagram的复杂导航:
- "带独立导航栈的标签栏"
- "所有流程使用流畅的协调器模式"
- "支持深度链接到任意页面"
- "从任何位置都能访问个人资料"
Red Flags — Recognize Over-Engineering Pressure
警示 — 识别过度设计的压力
If you hear ANY of these, STOP and evaluate:
- 🚩 "Let's build a full coordinator layer before any views" → Usually YAGNI
- 🚩 "We need a navigation architecture that handles anything" → Scope creep
- 🚩 "Instagram/TikTok does it this way" → They have 100+ engineers
如果你听到以下任何说法,请立即停止并评估:
- 🚩 "在开发任何视图前先构建完整的协调器层" → 通常属于YAGNI(You Aren't Gonna Need It,你不会需要它)
- 🚩 "我们需要一个能处理所有情况的导航架构" → 范围蔓延
- 🚩 "Instagram/TikTok就是这么做的" → 他们有100+工程师
Time Cost Comparison
时间成本对比
Option A: Over-Engineered Coordinator
选项A:过度设计的协调器
- Time to build coordinator layer: 3-5 days
- Time to maintain and debug: Ongoing
- Time when requirements change: Significant refactor
- 构建协调器层时间:3-5天
- 维护和调试时间:持续投入
- 需求变更时的时间:大量重构
Option B: Built-in Navigation + Simple Router
选项B:内置导航 + 简单路由器
- Time to implement Pattern 4 (TabView + NavigationStack): 2-3 hours
- Time to add Router if needed: 1-2 hours
- Time when requirements change: Incremental additions
- 实现模式4(TabView + NavigationStack)时间:2-3小时
- 如需添加路由器时间:1-2小时
- 需求变更时的时间:增量式添加
How to Push Back Professionally
专业的反驳话术
Step 1: Quantify Current Needs
步骤1:量化当前需求
"Let's list our actual navigation flows:
1. Home → Item Detail
2. Search → Results → Item Detail
3. Profile → Settings
That's 6 destinations. NavigationPath handles this natively.""我们先列出实际需要的导航流程:
1. 首页 → 项目详情
2. 搜索 → 结果 → 项目详情
3. 个人资料 → 设置
总共6个目标页面。NavigationPath原生就能处理这些。"Step 2: Show the Built-in Solution
步骤2:展示内置解决方案
"Here's our navigation with NavigationStack + NavigationPath:
[Show Pattern 1b code]
This gives us:
- Programmatic navigation ✓
- Deep linking ✓
- State restoration ✓
- Type safety ✓
Without a coordinator layer.""这是使用NavigationStack + NavigationPath实现的导航:
[展示模式1b代码]
它能提供:
- 程序化导航 ✓
- 深度链接 ✓
- 状态恢复 ✓
- 类型安全 ✓
无需协调器层。"Step 3: Offer Incremental Path
步骤3:提供增量方案
"If we find NavigationPath insufficient, we can add a Router
(Pattern 7) later. It's 30-45 minutes of work.
But let's start with the simpler solution and add complexity
only when we hit a real limitation.""如果发现NavigationPath不够用,我们之后可以添加路由器
(模式7),这只需要30-45分钟的工作量。
但我们先从更简单的方案开始,只有遇到真正的限制时再增加复杂度。"Real-World Example: 48-Hour Feature Push
真实案例:48小时功能交付
Scenario:
- PM: "We need deep linking for the campaign launch in 2 days"
- Lead: "Let's build a proper coordinator first"
- Time available: 16 working hours
Wrong approach:
- 8 hours: Build coordinator infrastructure
- 4 hours: Debug coordinator edge cases
- 4 hours: Rush deep linking on broken foundation
- Result: Buggy, deadline missed
Correct approach:
- 2 hours: Implement Pattern 1b (NavigationStack with deep linking)
- 1 hour: Test all deep link URLs
- 1 hour: Add SceneStorage restoration (Pattern 6)
- Result: Working deep links in 4 hours, 12 hours for polish/testing
场景:
- 产品经理:"我们需要在2天内为营销活动上线深度链接"
- 负责人:"先构建一个合适的协调器"
- 可用时间:16个工作小时
错误方案:
- 8小时:构建协调器基础设施
- 4小时:调试协调器边缘情况
- 4小时:在不稳定的基础上匆忙实现深度链接
- 结果:Bug多,错过截止日期
正确方案:
- 2小时:实现模式1b(带深度链接的NavigationStack)
- 1小时:测试所有深度链接URL
- 1小时:添加SceneStorage恢复(模式6)
- 结果:4小时内完成可用的深度链接,剩余12小时用于优化和测试
Pressure Scenario: "NavigationView Backward Compatibility"
压力场景:"NavigationView向后兼容"
The Problem
问题
Team lead says: "Let's use NavigationView so we support iOS 15"
团队负责人说:"我们使用NavigationView以支持iOS 15"
Red Flags
警示
- 🚩 NavigationView deprecated since iOS 16 (2022)
- 🚩 Different behavior across iOS versions causes bugs
- 🚩 No NavigationPath support — can't deep link properly
- 🚩 NavigationView自iOS 16起已被弃用(2022年)
- 🚩 不同iOS版本间的行为差异会引发Bug
- 🚩 不支持NavigationPath — 无法可靠实现深度链接
Data to Share
可分享的数据
iOS 16+ adoption: 95%+ of active devices (as of 2024)
iOS 15: < 5% and declining
NavigationView limitations:
- No programmatic path manipulation
- No type-safe navigation
- No built-in state restoration
- Behavior varies by iOS versioniOS 16+设备占比:95%+(截至2024年)
iOS 15:<5%且持续下降
NavigationView的局限性:
- 无程序化路径操作
- 无类型安全导航
- 无内置状态恢复
- 不同iOS版本行为不一致Push-Back Script
反驳话术
"NavigationView was deprecated in iOS 16 (2022). Here's the impact:
1. We lose NavigationPath — can't implement deep linking reliably
2. Behavior differs between iOS 15 and 16 — more bugs to maintain
3. iOS 15 is < 5% of users — we're adding complexity for small audience
Recommendation: Set deployment target to iOS 16, use NavigationStack.
If iOS 15 support is required, use NavigationStack with @available
checks and fallback UI for older devices.""NavigationView在iOS 16(2022年)已被弃用。影响如下:
1. 失去NavigationPath支持 — 无法可靠实现深度链接
2. iOS 15和16之间行为不同 — 需要维护更多Bug
3. iOS 15用户占比<5% — 我们为小众用户增加了复杂度
建议:将部署目标设为iOS 16,使用NavigationStack。
如果必须支持iOS 15,可使用NavigationStack并配合@available检查,为旧设备提供降级UI。"Code Review Checklist
代码评审检查清单
Navigation Architecture
导航架构
- Correct container for use case (Stack vs SplitView vs TabView)
- Value-based NavigationLink (not view-based)
- navigationDestination outside lazy containers
- Each tab has own NavigationStack (if tab-based)
- 根据使用场景选择了正确的容器(Stack vs SplitView vs TabView)
- 使用了基于值的NavigationLink(而非基于视图)
- navigationDestination放置在惰性容器外部
- 标签式应用中每个标签拥有独立的NavigationStack
State Management
状态管理
- NavigationPath in @State or @StateObject (not recreated in body)
- @MainActor isolation for navigation state (Swift 6)
- IDs stored for restoration (not full objects)
- Error handling for decode failures
- NavigationPath存储在@State或@StateObject中(而非在body中重建)
- 导航状态使用@MainActor隔离(Swift 6)
- 状态恢复存储的是ID(而非完整对象)
- 处理了解码失败的情况
Deep Linking
深度链接
- onOpenURL handler present
- Pop to root before building path
- Path built in correct order (parent → child)
- Missing data handled gracefully
- 存在onOpenURL处理器
- 构建路径前先返回根视图
- 按正确顺序构建路径(父视图 → 子视图)
- 优雅处理缺失的数据
iOS 26+ Features
iOS 26+功能
- No custom backgrounds interfering with Liquid Glass
- Bottom-aligned search working on iPhone
- Tab bar minimization if appropriate
- 无自定义背景干扰Liquid Glass
- iPhone上的底部搜索正常工作
- 按需启用标签栏最小化
Troubleshooting Quick Reference
故障排查速查表
| Symptom | Likely Cause | Pattern |
|---|---|---|
| Navigation doesn't respond to taps | NavigationLink outside NavigationStack | Check hierarchy |
| Double navigation on tap | Button wrapping NavigationLink | Remove Button wrapper |
| State lost on tab switch | Shared NavigationStack across tabs | Pattern 4 |
| State lost on background | No SceneStorage | Pattern 6 |
| Deep link shows wrong screen | Path built in wrong order | Pattern 1b |
| Crash on restore | Force unwrap decode | Handle errors gracefully |
| 症状 | 可能原因 | 对应模式 |
|---|---|---|
| 导航不响应点击 | NavigationLink在NavigationStack外部 | 检查层级结构 |
| 点击触发双重导航 | Button包裹了NavigationLink | 移除Button包裹 |
| 切换标签时状态丢失 | 所有标签共享同一个NavigationStack | 模式4 |
| 进入后台后状态丢失 | 未使用SceneStorage | 模式6 |
| 深度链接显示错误页面 | 路径构建顺序错误 | 模式1b |
| 恢复时崩溃 | 强制解包解码结果 | 优雅处理错误 |
Resources
资源
WWDC: 2022-10054, 2024-10147, 2025-256, 2025-323
Skills: axiom-swiftui-nav-diag, axiom-swiftui-nav-ref
Last Updated Based on WWDC 2022-2025 navigation sessions
Platforms iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, tvOS 18+
WWDC: 2022-10054, 2024-10147, 2025-256, 2025-323
技能: axiom-swiftui-nav-diag, axiom-swiftui-nav-ref
最后更新 基于WWDC 2022-2025导航相关会话
平台 iOS 18+, iPadOS 18+, macOS 15+, watchOS 11+, tvOS 18+