navigation-patterns
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseNavigation Patterns — Expert Decisions
导航模式——专家决策指南
Expert decision frameworks for SwiftUI navigation choices. Claude knows NavigationStack syntax — this skill provides judgment calls for architecture decisions and state management trade-offs.
SwiftUI导航选择的专家决策框架。Claude精通NavigationStack语法——本技能为架构决策和状态管理权衡提供专业判断。
Decision Trees
决策树
Navigation Architecture Selection
导航架构选型
How complex is your navigation?
├─ Simple (linear flows, 1-3 screens)
│ └─ NavigationStack with inline NavigationLink
│ No Router needed
│
├─ Medium (multiple flows, deep linking required)
│ └─ NavigationStack + Router (ObservableObject)
│ Centralized navigation state
│
└─ Complex (tabs with independent stacks, cross-tab navigation)
└─ Tab Coordinator + per-tab Routers
Each tab maintains own NavigationPath你的导航复杂度如何?
├─ 简单(线性流程,1-3个页面)
│ └─ 搭配内嵌NavigationLink的NavigationStack
│ 无需Router
│
├─ 中等(多流程,需深度链接)
│ └─ NavigationStack + Router(ObservableObject)
│ 集中式导航状态
│
└─ 复杂(带独立栈的标签页,跨标签导航)
└─ Tab Coordinator + 每个标签页独立Router
每个标签页维护自身的NavigationPathNavigationPath vs Typed Array
NavigationPath vs 类型化数组
Do you need heterogeneous routes?
├─ YES (different types in same stack)
│ └─ NavigationPath (type-erased)
│ path.append(User(...))
│ path.append(Product(...))
│
└─ NO (single route enum)
└─ @State var path: [Route] = []
Type-safe, debuggable, serializableRule: Prefer typed arrays unless you genuinely need mixed types. NavigationPath's type erasure makes debugging harder.
你需要异构路由吗?
├─ 是(同一栈中存在不同类型)
│ └─ NavigationPath(类型擦除)
│ path.append(User(...))
│ path.append(Product(...))
│
└─ 否(单一路由枚举)
└─ @State var path: [Route] = []
类型安全、可调试、可序列化规则:除非确实需要混合类型,否则优先选择类型化数组。NavigationPath的类型擦除会增加调试难度。
Deep Link Handling Strategy
深度链接处理策略
When does deep link arrive?
├─ App already running (warm start)
│ └─ Direct navigation via Router
│
└─ App launches from deep link (cold start)
└─ Is view hierarchy ready?
├─ YES → Navigate immediately
└─ NO → Queue pending deep link
Handle in root view's .onAppear深度链接何时到达?
├─ 应用已运行(热启动)
│ └─ 通过Router直接导航
│
└─ 应用从深度链接启动(冷启动)
└─ 视图层级是否就绪?
├─ 是 → 立即导航
└─ 否 → 队列化待处理深度链接
在根视图的.onAppear中处理Modal vs Push Selection
模态弹窗 vs 推送页面选型
Is the destination a self-contained flow?
├─ YES (can complete/cancel independently)
│ └─ Modal (.sheet or .fullScreenCover)
│ Examples: Settings, Compose, Login
│
└─ NO (part of current navigation hierarchy)
└─ Push (NavigationLink or path.append)
Examples: Detail views, drill-down目标页面是独立完整的流程吗?
├─ 是(可独立完成/取消)
│ └─ 模态弹窗(.sheet或.fullScreenCover)
│ 示例:设置、内容编辑、登录
│
└─ 否(属于当前导航层级的一部分)
└─ 推送页面(NavigationLink或path.append)
示例:详情页、层级钻取NEVER Do
绝对禁忌
NavigationPath State
NavigationPath状态管理
NEVER store NavigationPath in ViewModel without careful consideration:
swift
// ❌ ViewModel owns navigation — couples business logic to navigation
@MainActor
final class HomeViewModel: ObservableObject {
@Published var path = NavigationPath() // Wrong layer!
}
// ✅ Router/Coordinator owns navigation, ViewModel owns data
@MainActor
final class Router: ObservableObject {
@Published var path = NavigationPath()
}
@MainActor
final class HomeViewModel: ObservableObject {
@Published var items: [Item] = [] // Data only
}NEVER use NavigationPath across tabs:
swift
// ❌ Shared path across tabs — navigation becomes unpredictable
struct MainTabView: View {
@StateObject var router = Router() // Single router!
var body: some View {
TabView {
// Both tabs share same path — chaos
}
}
}
// ✅ Each tab has independent navigation stack
struct MainTabView: View {
@StateObject var homeRouter = Router()
@StateObject var searchRouter = Router()
var body: some View {
TabView {
NavigationStack(path: $homeRouter.path) { ... }
NavigationStack(path: $searchRouter.path) { ... }
}
}
}NEVER forget to handle deep links arriving before view hierarchy:
swift
// ❌ Race condition — navigation may fail silently
@main
struct MyApp: App {
@StateObject var router = Router()
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
router.handle(url) // View may not exist yet!
}
}
}
}
// ✅ Queue deep link for deferred handling
@main
struct MyApp: App {
@StateObject var router = Router()
@State private var pendingDeepLink: URL?
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
if let url = pendingDeepLink {
router.handle(url)
pendingDeepLink = nil
}
}
.onOpenURL { url in
pendingDeepLink = url
}
}
}
}绝对不要在未充分考虑的情况下将NavigationPath存储在ViewModel中:
swift
// ❌ ViewModel拥有导航逻辑——业务逻辑与导航耦合
@MainActor
final class HomeViewModel: ObservableObject {
@Published var path = NavigationPath() // 错误的层级!
}
// ✅ Router/Coordinator拥有导航逻辑,ViewModel仅管理数据
@MainActor
final class Router: ObservableObject {
@Published var path = NavigationPath()
}
@MainActor
final class HomeViewModel: ObservableObject {
@Published var items: [Item] = [] // 仅管理数据
}绝对不要跨标签页共享NavigationPath:
swift
// ❌ 跨标签页共享路径——导航变得不可预测
struct MainTabView: View {
@StateObject var router = Router() // 单一Router!
var body: some View {
TabView {
// 两个标签页共享同一路径——混乱不堪
}
}
}
// ✅ 每个标签页拥有独立的导航栈
struct MainTabView: View {
@StateObject var homeRouter = Router()
@StateObject var searchRouter = Router()
var body: some View {
TabView {
NavigationStack(path: $homeRouter.path) { ... }
NavigationStack(path: $searchRouter.path) { ... }
}
}
}绝对不要忘记处理视图层级就绪前到达的深度链接:
swift
// ❌ 竞态条件——导航可能静默失败
@main
struct MyApp: App {
@StateObject var router = Router()
var body: some Scene {
WindowGroup {
ContentView()
.onOpenURL { url in
router.handle(url) // 视图可能尚未存在!
}
}
}
}
// ✅ 队列化深度链接以延迟处理
@main
struct MyApp: App {
@StateObject var router = Router()
@State private var pendingDeepLink: URL?
var body: some Scene {
WindowGroup {
ContentView()
.onAppear {
if let url = pendingDeepLink {
router.handle(url)
pendingDeepLink = nil
}
}
.onOpenURL { url in
pendingDeepLink = url
}
}
}
}Route Design
路由设计
NEVER use stringly-typed routes:
swift
// ❌ No compile-time safety, typos cause runtime failures
func navigate(to screen: String) {
switch screen {
case "profile": ...
case "setings": ... // Typo — silent failure
}
}
// ✅ Enum routes with associated values
enum Route: Hashable {
case profile(userId: String)
case settings
}NEVER put navigation logic in Views:
swift
// ❌ View knows too much about app structure
struct ItemRow: View {
var body: some View {
NavigationLink {
ItemDetailView(item: item) // View creates destination
} label: {
Text(item.name)
}
}
}
// ✅ Delegate navigation to Router
struct ItemRow: View {
@EnvironmentObject var router: Router
var body: some View {
Button(item.name) {
router.navigate(to: .itemDetail(item.id))
}
}
}绝对不要使用字符串类型的路由:
swift
// ❌ 无编译时安全保障,拼写错误导致运行时失败
func navigate(to screen: String) {
switch screen {
case "profile": ...
case "setings": ... // 拼写错误——静默失败
}
}
// ✅ 带关联值的枚举路由
enum Route: Hashable {
case profile(userId: String)
case settings
}绝对不要将导航逻辑放在视图中:
swift
// ❌ 视图对应用结构了解过多
struct ItemRow: View {
var body: some View {
NavigationLink {
ItemDetailView(item: item) // 视图创建目标页面
} label: {
Text(item.name)
}
}
}
// ✅ 将导航委托给Router
struct ItemRow: View {
@EnvironmentObject var router: Router
var body: some View {
Button(item.name) {
router.navigate(to: .itemDetail(item.id))
}
}
}Navigation State Persistence
导航状态持久化
NEVER lose navigation state on app termination without consideration:
swift
// ❌ User loses their place when app is killed
@StateObject var router = Router() // State lost on terminate
// ✅ Persist for important flows (optional based on UX needs)
@SceneStorage("navigationPath") private var pathData: Data?
var body: some View {
NavigationStack(path: $router.path) { ... }
.onAppear { router.restore(from: pathData) }
.onChange(of: router.path) { pathData = router.serialize() }
}绝对不要在未考虑用户体验的情况下,应用终止时丢失导航状态:
swift
// ❌ 应用终止时用户丢失当前位置
@StateObject var router = Router() // 终止时状态丢失
// ✅ 为重要流程持久化状态(根据UX需求可选)
@SceneStorage("navigationPath") private var pathData: Data?
var body: some View {
NavigationStack(path: $router.path) { ... }
.onAppear { router.restore(from: pathData) }
.onChange(of: router.path) { pathData = router.serialize() }
}Essential Patterns
必备模式
Type-Safe Router
类型安全Router
swift
@MainActor
final class Router: ObservableObject {
enum Route: Hashable {
case userList
case userDetail(userId: String)
case settings
case settingsSection(SettingsSection)
}
@Published var path: [Route] = []
func navigate(to route: Route) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path.removeAll()
}
func replaceStack(with routes: [Route]) {
path = routes
}
@ViewBuilder
func destination(for route: Route) -> some View {
switch route {
case .userList:
UserListView()
case .userDetail(let userId):
UserDetailView(userId: userId)
case .settings:
SettingsView()
case .settingsSection(let section):
SettingsSectionView(section: section)
}
}
}swift
@MainActor
final class Router: ObservableObject {
enum Route: Hashable {
case userList
case userDetail(userId: String)
case settings
case settingsSection(SettingsSection)
}
@Published var path: [Route] = []
func navigate(to route: Route) {
path.append(route)
}
func pop() {
guard !path.isEmpty else { return }
path.removeLast()
}
func popToRoot() {
path.removeAll()
}
func replaceStack(with routes: [Route]) {
path = routes
}
@ViewBuilder
func destination(for route: Route) -> some View {
switch route {
case .userList:
UserListView()
case .userDetail(let userId):
UserDetailView(userId: userId)
case .settings:
SettingsView()
case .settingsSection(let section):
SettingsSectionView(section: section)
}
}
}Deep Link Handler
深度链接处理器
swift
enum DeepLink {
case user(id: String)
case product(id: String)
case settings
init?(url: URL) {
guard let scheme = url.scheme,
["myapp", "https"].contains(scheme) else { return nil }
let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let components = path.components(separatedBy: "/")
switch components.first {
case "user":
guard components.count > 1 else { return nil }
self = .user(id: components[1])
case "product":
guard components.count > 1 else { return nil }
self = .product(id: components[1])
case "settings":
self = .settings
default:
return nil
}
}
}
extension Router {
func handle(_ deepLink: DeepLink) {
popToRoot()
switch deepLink {
case .user(let id):
navigate(to: .userList)
navigate(to: .userDetail(userId: id))
case .product(let id):
navigate(to: .productDetail(productId: id))
case .settings:
navigate(to: .settings)
}
}
}swift
enum DeepLink {
case user(id: String)
case product(id: String)
case settings
init?(url: URL) {
guard let scheme = url.scheme,
["myapp", "https"].contains(scheme) else { return nil }
let path = url.path.trimmingCharacters(in: CharacterSet(charactersIn: "/"))
let components = path.components(separatedBy: "/")
switch components.first {
case "user":
guard components.count > 1 else { return nil }
self = .user(id: components[1])
case "product":
guard components.count > 1 else { return nil }
self = .product(id: components[1])
case "settings":
self = .settings
default:
return nil
}
}
}
extension Router {
func handle(_ deepLink: DeepLink) {
popToRoot()
switch deepLink {
case .user(let id):
navigate(to: .userList)
navigate(to: .userDetail(userId: id))
case .product(let id):
navigate(to: .productDetail(productId: id))
case .settings:
navigate(to: .settings)
}
}
}Tab + Navigation Coordination
标签栏+导航协调器
swift
struct MainTabView: View {
@State private var selectedTab: Tab = .home
@StateObject private var homeRouter = Router()
@StateObject private var profileRouter = Router()
enum Tab { case home, search, profile }
var body: some View {
TabView(selection: $selectedTab) {
NavigationStack(path: $homeRouter.path) {
HomeView()
.navigationDestination(for: Router.Route.self) { route in
homeRouter.destination(for: route)
}
}
.tag(Tab.home)
.environmentObject(homeRouter)
NavigationStack(path: $profileRouter.path) {
ProfileView()
.navigationDestination(for: Router.Route.self) { route in
profileRouter.destination(for: route)
}
}
.tag(Tab.profile)
.environmentObject(profileRouter)
}
}
// Pop to root on tab re-selection
func tabSelected(_ tab: Tab) {
if selectedTab == tab {
switch tab {
case .home: homeRouter.popToRoot()
case .profile: profileRouter.popToRoot()
case .search: break
}
}
selectedTab = tab
}
}swift
struct MainTabView: View {
@State private var selectedTab: Tab = .home
@StateObject private var homeRouter = Router()
@StateObject private var profileRouter = Router()
enum Tab { case home, search, profile }
var body: some View {
TabView(selection: $selectedTab) {
NavigationStack(path: $homeRouter.path) {
HomeView()
.navigationDestination(for: Router.Route.self) { route in
homeRouter.destination(for: route)
}
}
.tag(Tab.home)
.environmentObject(homeRouter)
NavigationStack(path: $profileRouter.path) {
ProfileView()
.navigationDestination(for: Router.Route.self) { route in
profileRouter.destination(for: route)
}
}
.tag(Tab.profile)
.environmentObject(profileRouter)
}
}
// 重新选中标签栏时返回根页面
func tabSelected(_ tab: Tab) {
if selectedTab == tab {
switch tab {
case .home: homeRouter.popToRoot()
case .profile: profileRouter.popToRoot()
case .search: break
}
}
selectedTab = tab
}
}Quick Reference
快速参考
Navigation Architecture Comparison
导航架构对比
| Pattern | Complexity | Deep Link Support | Testability |
|---|---|---|---|
| Inline NavigationLink | Low | Manual | Low |
| Router with typed array | Medium | Good | High |
| NavigationPath | Medium | Good | Medium |
| Coordinator Pattern | High | Excellent | Excellent |
| 模式 | 复杂度 | 深度链接支持 | 可测试性 |
|---|---|---|---|
| 内嵌NavigationLink | 低 | 手动实现 | 低 |
| 搭配类型化数组的Router | 中 | 良好 | 高 |
| NavigationPath | 中 | 良好 | 中 |
| Coordinator模式 | 高 | 优秀 | 优秀 |
When to Use Each Modal Type
各模态弹窗类型适用场景
| Modal Type | Use For |
|---|---|
| Secondary tasks, can dismiss |
| Immersive flows (onboarding, login) |
| Critical decisions |
| Action choices |
| 模态类型 | 适用场景 |
|---|---|
| 次要任务,可关闭 |
| 沉浸式流程(引导页、登录) |
| 关键决策 |
| 操作选择 |
Red Flags
警示信号
| Smell | Problem | Fix |
|---|---|---|
| NavigationPath across tabs | State confusion | Per-tab routers |
| View creates destination directly | Tight coupling | Router pattern |
| String-based routing | No compile safety | Enum routes |
| Deep link ignored on cold start | Race condition | Pending URL queue |
| ViewModel owns NavigationPath | Layer violation | Router owns navigation |
| No popToRoot on tab re-tap | UX expectation | Handle tab selection |
| 问题迹象 | 核心问题 | 修复方案 |
|---|---|---|
| 跨标签页共享NavigationPath | 状态混乱 | 每个标签页独立Router |
| 视图直接创建目标页面 | 紧耦合 | Router模式 |
| 字符串类型路由 | 无编译安全保障 | 枚举路由 |
| 冷启动时深度链接被忽略 | 竞态条件 | 待处理URL队列 |
| ViewModel拥有NavigationPath | 层级违规 | Router管理导航 |
| 重新点击标签栏不返回根页面 | 不符合UX预期 | 处理标签栏选择事件 |