axiom-swiftui-nav by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-swiftui-nav在以下情况下使用:
axiom-swiftui-nav-diag 对导航失败进行系统性故障排除axiom-swiftui-nav-ref 获取全面的 API 参考(包括标签页自定义、iOS 26+ 功能)以及所有 WWDC 示例这些是开发者提出的真实问题,本技能旨在解答:
-> 该技能根据设备目标、内容层次深度和多平台要求提供决策树
-> 该技能展示了用于推送、弹出、弹出到根视图和深度链接的 NavigationPath 操作模式
-> 该技能涵盖了 URL 解析模式、路径构建顺序以及与 onOpenURL 相关的时间问题
-> 该技能演示了 Codable NavigationPath、SceneStorage 持久化和抗崩溃的恢复机制
-> 该技能提供了路由器模式示例,并指导何时使用协调器能增加价值而非复杂性
如果你正在做以下任何一项,请立即停止并使用本技能中的模式:
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// ❌ 错误 —— 已弃用,在 iOS 16+ 上行为不同
NavigationView {
List { ... }
}
.navigationViewStyle(.stack)
失败原因 NavigationView 自 iOS 16 起已弃用。它缺乏 NavigationPath 支持,使得程序化导航和深度链接不可靠。不同 iOS 版本间的行为差异会导致错误。
// ❌ 错误 —— 无法通过程序控制
NavigationLink("Recipe") {
RecipeDetail(recipe: recipe) // 视图目标,无值
}
失败原因 基于视图的链接无法通过程序控制。无法深度链接或弹出到此目标。自 iOS 16 起已弃用。
// ❌ 错误 —— 需要时可能未加载
LazyVGrid(columns: columns) {
ForEach(items) { item in
NavigationLink(value: item) { ... }
.navigationDestination(for: Item.self) { item in // 不要这样做
ItemDetail(item: item)
}
}
}
失败原因 惰性容器不会立即加载所有视图。navigationDestination 可能对 NavigationStack 不可见,导致导航静默失败。
// ❌ 错误 —— 数据重复,恢复时可能过时
class NavigationModel: Codable {
var path: [Recipe] = [] // 完整的 Recipe 对象
}
失败原因 重复了模型中已有的数据。恢复时,Recipe 数据可能已过时(在其他地方被编辑/删除)。请使用 ID 并解析为当前数据。
// ❌ 错误 —— 在非主线程更新 UI
Task.detached {
await viewModel.path.append(recipe) // 后台线程
}
失败原因 NavigationPath 绑定到 UI。修改必须在 MainActor 上发生,否则导航状态会损坏。可能导致崩溃或静默失败。
// ❌ 错误 —— 未进行 MainActor 隔离
class Router: ObservableObject {
@Published var path = NavigationPath() // 没有 @MainActor
}
失败原因 在 Swift 6 严格并发中,从 SwiftUI 视图访问的 @Published 属性需要 MainActor 隔离。会导致数据竞争警告和潜在的崩溃。
// ❌ 错误 —— 跨标签页共享 NavigationPath
TabView {
Tab("Home") { HomeView() }
Tab("Settings") { SettingsView() }
}
// 所有标签页共享同一个 NavigationStack —— 错误!
失败原因 每个标签页应有自己的 NavigationStack,以便在切换标签页时保留导航状态。共享状态会导致混乱的用户体验。
// ❌ 错误 —— 在无效数据时崩溃
let path = NavigationPath(try! decoder.decode(NavigationPath.CodableRepresentation.self, from: data))
失败原因 用户可能删除了路径中的项目。架构可能已更改。强制解包会导致恢复时崩溃。
在实现导航之前,务必完成以下步骤:
// 步骤 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?
需要导航吗?
├─ 多列界面(iPad/Mac 为主)?
│ └─ NavigationSplitView
│ ├─ 详情列中需要向下钻取?
│ │ └─ 在详情中使用 NavigationStack(模式 3)
│ └─ 仅选择详情?
│ └─ 仅使用选择绑定(模式 2)
├─ 基于标签页的应用?
│ └─ TabView
│ ├─ 每个标签页都需要向下钻取?
│ │ └─ 每个标签页使用 NavigationStack(模式 4)
│ └─ iPad 侧边栏体验?
│ └─ .tabViewStyle(.sidebarAdaptable)(模式 5)
└─ 单列栈?
└─ NavigationStack
├─ 需要深度链接?
│ └─ 使用 NavigationPath(模式 1b)
└─ 简单的推送/弹出?
└─ 类型化数组路径(模式 1a)
需要状态恢复吗?
└─ SceneStorage + Codable NavigationPath(模式 6)
需要协调器抽象吗?
├─ 复杂的条件流程?
├─ 需要测试导航逻辑?
├─ 在许多屏幕间共享导航?
└─ 任何一项为是 → 路由器模式(模式 7)
所有都为否 → 直接使用 NavigationPath
适用场景:简单的推送/弹出导航,所有目标类型相同
时间成本:5-10 分钟
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:) 放在惰性容器外部适用场景:多种目标类型,基于 URL 的深度链接
时间成本:15-20 分钟
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适用场景:多列布局,其中详情显示选定的项目
时间成本:10-15 分钟
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")
}
}
}
}
关键点:
selection: $binding 连接到列选择适用场景:多列布局,详情中具有向下钻取能力
时间成本:20-25 分钟
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")
}
}
}
}
}
关键点:
适用场景:基于标签页的应用,每个标签页都有自己的导航
时间成本:15-20 分钟
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()
}
}
}
}
}
关键点:
适用场景:iPhone 上使用标签栏,iPad 上使用侧边栏
时间成本:20-25 分钟
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)
}
}
关键点:
.tabViewStyle(.sidebarAdaptable) 在 iPad 上启用侧边栏TabSection 在侧边栏中创建可折叠的组Tab(role: .search) 获得特殊位置适用场景:在应用启动之间保留导航状态
时间成本:25-30 分钟
@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
}
}
}
}
关键点:
@MainActor 用于 Swift 6 并发安全compactMap 以处理已删除的项目适用场景:复杂的导航逻辑,需要可测试性
时间成本:30-45 分钟
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)
}
}
协调器何时增加价值:
协调器何时增加复杂性而无价值:
// ❌ 错误 —— 嵌套栈
NavigationStack {
SomeView()
.sheet(isPresented: $showSheet) {
NavigationStack { // 创建单独的栈 —— 令人困惑
SheetContent()
}
}
}
问题 两个导航栈会造成混乱的用户体验。返回按钮行为不明确。修复 尽可能使用单个 NavigationStack,在不嵌套导航的情况下呈现 sheet。
// ❌ 错误 —— 双重导航触发
Button("Go") {
// 某些操作
} label: {
NavigationLink(value: item) { // 在按钮和链接上都会触发
Text("Item")
}
}
问题 Button 和 NavigationLink 都会响应点击。修复 仅使用 NavigationLink,如果需要,将操作放在 .simultaneousGesture 中。
// ❌ 错误 —— 每次渲染都重新创建
var body: some View {
let path = NavigationPath() // 每次渲染都重置!
NavigationStack(path: .constant(path)) { ... }
}
问题 每次渲染都会重新创建路径,导航状态丢失。修复 对导航状态使用 @State 或 @StateObject。
产品/设计要求实现类似 Instagram 的复杂导航:
如果你听到以下任何一项,请停止并评估:
"让我们列出实际的导航流程:
1. 首页 → 项目详情
2. 搜索 → 结果 → 项目详情
3. 个人资料 → 设置
这是 6 个目标。NavigationPath 原生支持这个。"
"这是使用 NavigationStack + NavigationPath 的导航:
[展示模式 1b 代码]
这为我们提供了:
- 程序化导航 ✓
- 深度链接 ✓
- 状态恢复 ✓
- 类型安全 ✓
无需协调器层。"
"如果我们发现 NavigationPath 不够用,可以稍后添加路由器
(模式 7)。这只需要 30-45 分钟的工作。
但让我们从更简单的解决方案开始,只有当我们遇到
真正的限制时才增加复杂性。"
场景:
错误方法:
正确方法:
团队负责人说:"让我们使用 NavigationView,这样我们就能支持 iOS 15"
iOS 16+ 采用率:95%+ 的活跃设备(截至 2024 年)
iOS 15:< 5% 且持续下降
NavigationView 限制:
- 无法通过程序操作路径
- 无类型安全导航
- 无内置状态恢复
- 行为因 iOS 版本而异
"NavigationView 在 iOS 16(2022 年)中已被弃用。以下是影响:
1. 我们失去了 NavigationPath —— 无法可靠地实现深度链接
2. iOS 15 和 16 之间的行为不同 —— 需要维护更多错误
3. iOS 15 用户占比 < 5% —— 我们为少量用户增加了复杂性
建议:将部署目标设置为 iOS 16,使用 NavigationStack。
如果需要支持 iOS 15,请使用 NavigationStack 配合 @available
检查,并为旧设备提供备用 UI。"
| 症状 | 可能原因 | 模式 |
|---|---|---|
| 导航不响应点击 | NavigationLink 在 NavigationStack 外部 | 检查层次结构 |
| 点击时双重导航 | Button 包装了 NavigationLink | 移除 Button 包装器 |
| 切换标签页时状态丢失 | 跨标签页共享 NavigationStack | 模式 4 |
| 进入后台时状态丢失 | 没有 SceneStorage | 模式 6 |
| 深度链接显示错误屏幕 | 路径构建顺序错误 | 模式 1b |
| 恢复时崩溃 | 强制解包解码 | 优雅处理错误 |
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+
每周安装次数
103
代码仓库
GitHub 星标数
601
首次出现
2026 年 1 月 21 日
安全审计
安装于
opencode89
codex83
claude-code81
gemini-cli78
cursor78
github-copilot75
Use when:
axiom-swiftui-nav-diag for systematic troubleshooting of navigation failuresaxiom-swiftui-nav-ref for comprehensive API reference (including Tab customization, iOS 26+ features) with all WWDC examplesThese are real questions developers ask that this skill is designed to answer:
-> The skill provides a decision tree based on device targets, content hierarchy depth, and multiplatform requirements
-> The skill shows NavigationPath manipulation patterns for push, pop, pop-to-root, and deep linking
-> The skill covers URL parsing patterns, path construction order, and timing issues with onOpenURL
-> The skill demonstrates Codable NavigationPath, SceneStorage persistence, and crash-resistant restoration
-> The skill provides Router pattern examples alongside guidance on when coordinators add value vs complexity
If you're doing ANY of these, STOP and use the patterns in this skill:
// ❌ 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.
// ❌ 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.
// ❌ 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.
// ❌ 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.
// ❌ 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.
// ❌ 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.
// ❌ 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.
// ❌ 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.
ALWAYS complete these steps before implementing navigation:
// 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?
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
When : Simple push/pop navigation, all destinations same type
Time cost : 5-10 min
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:
[Recipe] when all values are same typeNavigationLink(title, value:)navigationDestination(for:) outside lazy containersWhen : Multiple destination types, URL-based deep linking
Time cost : 15-20 min
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:
NavigationPath for heterogeneous typesWhen : Multi-column layout where detail shows selected item
Time cost : 10-15 min
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:
selection: $binding on List connects to column selectionWhen : Multi-column with drill-down capability in detail
Time cost : 20-25 min
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:
When : Tab-based app where each tab has its own navigation
Time cost : 15-20 min
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:
When : Tab bar on iPhone, sidebar on iPad
Time cost : 20-25 min
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:
.tabViewStyle(.sidebarAdaptable) enables sidebar on iPadTabSection creates collapsible groups in sidebarTab(role: .search) gets special placementWhen : Preserve navigation state across app launches
Time cost : 25-30 min
@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:
@MainActor for Swift 6 concurrency safetycompactMap when resolving IDs to handle deleted itemsWhen : Complex navigation logic, need testability
Time cost : 30-45 min
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:
When coordinators add complexity without value:
// ❌ 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.
// ❌ 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 .simultaneousGesture if needed.
// ❌ 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 @State or @StateObject for navigation state.
Product/design asks for complex navigation like Instagram:
If you hear ANY of these, STOP and evaluate :
"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."
"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."
"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."
Scenario:
Wrong approach:
Correct approach:
Team lead says: "Let's use NavigationView so we support iOS 15"
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 version
"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."
| 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 |
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+
Weekly Installs
103
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode89
codex83
claude-code81
gemini-cli78
cursor78
github-copilot75
Excel 数据分析器 - 自动检测数据结构、质量问题与统计模式,生成数据清洗报告
203 周安装
Elastic Cloud 云网络安全策略管理:IP过滤器与VPC过滤器配置指南
205 周安装
YouTube频道API工具 - 免费获取频道ID、最新视频和分页列表 | TranscriptAPI
70 周安装
Firecrawl 网页抓取工具:AI 友好的 Markdown 转换与反爬虫处理
199 周安装
阿里云 OpenClaw 最小化冒烟测试设置指南 - 验证 CLI、插件与网关状态
199 周安装
移动端离线支持开发指南:React Native/iOS/Android离线存储与同步最佳实践
199 周安装