@kata-sh/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +6 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +56 -0
- package/dist/loader.d.ts +2 -0
- package/dist/loader.js +95 -0
- package/dist/resource-loader.d.ts +18 -0
- package/dist/resource-loader.js +50 -0
- package/dist/wizard.d.ts +15 -0
- package/dist/wizard.js +159 -0
- package/package.json +50 -21
- package/pkg/dist/modes/interactive/theme/dark.json +85 -0
- package/pkg/dist/modes/interactive/theme/light.json +84 -0
- package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
- package/pkg/dist/modes/interactive/theme/theme.js +949 -0
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
- package/pkg/package.json +8 -0
- package/scripts/postinstall.js +45 -0
- package/src/resources/AGENTS.md +108 -0
- package/src/resources/KATA-WORKFLOW.md +661 -0
- package/src/resources/agents/researcher.md +29 -0
- package/src/resources/agents/scout.md +56 -0
- package/src/resources/agents/worker.md +31 -0
- package/src/resources/extensions/ask-user-questions.ts +200 -0
- package/src/resources/extensions/bg-shell/index.ts +2758 -0
- package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
- package/src/resources/extensions/browser-tools/core.js +1057 -0
- package/src/resources/extensions/browser-tools/index.ts +4916 -0
- package/src/resources/extensions/browser-tools/package.json +20 -0
- package/src/resources/extensions/context7/index.ts +428 -0
- package/src/resources/extensions/context7/package.json +11 -0
- package/src/resources/extensions/get-secrets-from-user.ts +352 -0
- package/src/resources/extensions/github/formatters.ts +207 -0
- package/src/resources/extensions/github/gh-api.ts +537 -0
- package/src/resources/extensions/github/index.ts +778 -0
- package/src/resources/extensions/kata/activity-log.ts +88 -0
- package/src/resources/extensions/kata/auto.ts +2786 -0
- package/src/resources/extensions/kata/commands.ts +355 -0
- package/src/resources/extensions/kata/crash-recovery.ts +85 -0
- package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
- package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
- package/src/resources/extensions/kata/doctor.ts +683 -0
- package/src/resources/extensions/kata/files.ts +730 -0
- package/src/resources/extensions/kata/gitignore.ts +165 -0
- package/src/resources/extensions/kata/guided-flow.ts +976 -0
- package/src/resources/extensions/kata/index.ts +556 -0
- package/src/resources/extensions/kata/metrics.ts +397 -0
- package/src/resources/extensions/kata/observability-validator.ts +408 -0
- package/src/resources/extensions/kata/package.json +11 -0
- package/src/resources/extensions/kata/paths.ts +346 -0
- package/src/resources/extensions/kata/preferences.ts +695 -0
- package/src/resources/extensions/kata/prompt-loader.ts +50 -0
- package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
- package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
- package/src/resources/extensions/kata/prompts/discuss.md +151 -0
- package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
- package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
- package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
- package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
- package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
- package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
- package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
- package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
- package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
- package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
- package/src/resources/extensions/kata/prompts/queue.md +85 -0
- package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
- package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
- package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
- package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
- package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
- package/src/resources/extensions/kata/prompts/system.md +341 -0
- package/src/resources/extensions/kata/session-forensics.ts +550 -0
- package/src/resources/extensions/kata/skill-discovery.ts +137 -0
- package/src/resources/extensions/kata/state.ts +509 -0
- package/src/resources/extensions/kata/templates/context.md +76 -0
- package/src/resources/extensions/kata/templates/decisions.md +8 -0
- package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
- package/src/resources/extensions/kata/templates/plan.md +133 -0
- package/src/resources/extensions/kata/templates/preferences.md +15 -0
- package/src/resources/extensions/kata/templates/project.md +31 -0
- package/src/resources/extensions/kata/templates/reassessment.md +28 -0
- package/src/resources/extensions/kata/templates/requirements.md +81 -0
- package/src/resources/extensions/kata/templates/research.md +46 -0
- package/src/resources/extensions/kata/templates/roadmap.md +118 -0
- package/src/resources/extensions/kata/templates/slice-context.md +58 -0
- package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
- package/src/resources/extensions/kata/templates/state.md +19 -0
- package/src/resources/extensions/kata/templates/task-plan.md +52 -0
- package/src/resources/extensions/kata/templates/task-summary.md +57 -0
- package/src/resources/extensions/kata/templates/uat.md +54 -0
- package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
- package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
- package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
- package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
- package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
- package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
- package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
- package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
- package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
- package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
- package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
- package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
- package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
- package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
- package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
- package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
- package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
- package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
- package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
- package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
- package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
- package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
- package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
- package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
- package/src/resources/extensions/kata/types.ts +159 -0
- package/src/resources/extensions/kata/unit-runtime.ts +163 -0
- package/src/resources/extensions/kata/workspace-index.ts +203 -0
- package/src/resources/extensions/kata/worktree.ts +182 -0
- package/src/resources/extensions/mac-tools/index.ts +852 -0
- package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
- package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
- package/src/resources/extensions/search-the-web/cache.ts +78 -0
- package/src/resources/extensions/search-the-web/format.ts +258 -0
- package/src/resources/extensions/search-the-web/http.ts +238 -0
- package/src/resources/extensions/search-the-web/index.ts +68 -0
- package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
- package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
- package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
- package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
- package/src/resources/extensions/shared/confirm-ui.ts +126 -0
- package/src/resources/extensions/shared/interview-ui.ts +822 -0
- package/src/resources/extensions/shared/next-action-ui.ts +235 -0
- package/src/resources/extensions/shared/progress-widget.ts +282 -0
- package/src/resources/extensions/shared/thinking-widget.ts +107 -0
- package/src/resources/extensions/shared/ui.ts +400 -0
- package/src/resources/extensions/shared/wizard-ui.ts +551 -0
- package/src/resources/extensions/slash-commands/audit.ts +92 -0
- package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
- package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
- package/src/resources/extensions/slash-commands/index.ts +12 -0
- package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
- package/src/resources/extensions/subagent/agents.ts +126 -0
- package/src/resources/extensions/subagent/index.ts +1293 -0
- package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
- package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
- package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
- package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
- package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
- package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
- package/src/resources/skills/frontend-design/SKILL.md +45 -0
- package/src/resources/skills/swiftui/SKILL.md +208 -0
- package/src/resources/skills/swiftui/references/animations.md +921 -0
- package/src/resources/skills/swiftui/references/architecture.md +1561 -0
- package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
- package/src/resources/skills/swiftui/references/navigation.md +1492 -0
- package/src/resources/skills/swiftui/references/networking-async.md +214 -0
- package/src/resources/skills/swiftui/references/performance.md +1706 -0
- package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
- package/src/resources/skills/swiftui/references/state-management.md +1443 -0
- package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
- package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
- package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
- package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
- package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
- package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
- package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
- package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
- package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
- package/dist/commands/task.d.ts +0 -9
- package/dist/commands/task.d.ts.map +0 -1
- package/dist/commands/task.js +0 -129
- package/dist/commands/task.js.map +0 -1
- package/dist/commands/task.test.d.ts +0 -2
- package/dist/commands/task.test.d.ts.map +0 -1
- package/dist/commands/task.test.js +0 -169
- package/dist/commands/task.test.js.map +0 -1
- package/dist/e2e/task-e2e.test.d.ts +0 -2
- package/dist/e2e/task-e2e.test.d.ts.map +0 -1
- package/dist/e2e/task-e2e.test.js +0 -173
- package/dist/e2e/task-e2e.test.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -93
- package/dist/index.js.map +0 -1
- package/dist/slug.d.ts +0 -2
- package/dist/slug.d.ts.map +0 -1
- package/dist/slug.js +0 -12
- package/dist/slug.js.map +0 -1
- package/dist/slug.test.d.ts +0 -2
- package/dist/slug.test.d.ts.map +0 -1
- package/dist/slug.test.js +0 -32
- package/dist/slug.test.js.map +0 -1
|
@@ -0,0 +1,1492 @@
|
|
|
1
|
+
<overview>
|
|
2
|
+
SwiftUI navigation has evolved significantly with NavigationStack (iOS 16+) replacing the deprecated NavigationView. The modern navigation model provides type-safe, programmatic control while supporting both user-driven and code-driven navigation patterns.
|
|
3
|
+
|
|
4
|
+
**Key insight:** NavigationStack with NavigationPath provides a stack-based navigation system where you can programmatically manipulate the navigation hierarchy while SwiftUI keeps the UI in sync automatically.
|
|
5
|
+
|
|
6
|
+
**Read this file when:** Building multi-screen apps, implementing deep linking, managing programmatic navigation, presenting sheets and modals, or setting up tab-based navigation.
|
|
7
|
+
|
|
8
|
+
**Related files:**
|
|
9
|
+
- architecture.md - Coordinator pattern for complex navigation flows
|
|
10
|
+
- state-management.md - Managing navigation state with @Observable
|
|
11
|
+
- platform-integration.md - Platform-specific navigation differences (iOS vs macOS)
|
|
12
|
+
</overview>
|
|
13
|
+
|
|
14
|
+
<navigation_stack>
|
|
15
|
+
## NavigationStack
|
|
16
|
+
|
|
17
|
+
NavigationStack manages a stack of views with type-safe routing. It replaces the deprecated NavigationView and provides better programmatic control.
|
|
18
|
+
|
|
19
|
+
**Basic usage with NavigationLink:**
|
|
20
|
+
```swift
|
|
21
|
+
struct ContentView: View {
|
|
22
|
+
var body: some View {
|
|
23
|
+
NavigationStack {
|
|
24
|
+
List {
|
|
25
|
+
NavigationLink("Details", value: "details")
|
|
26
|
+
NavigationLink("Settings", value: "settings")
|
|
27
|
+
}
|
|
28
|
+
.navigationTitle("Home")
|
|
29
|
+
.navigationDestination(for: String.self) { value in
|
|
30
|
+
Text("Showing: \(value)")
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**With NavigationPath for programmatic navigation:**
|
|
38
|
+
```swift
|
|
39
|
+
struct ContentView: View {
|
|
40
|
+
@State private var path = NavigationPath()
|
|
41
|
+
|
|
42
|
+
var body: some View {
|
|
43
|
+
NavigationStack(path: $path) {
|
|
44
|
+
VStack {
|
|
45
|
+
Button("Go to Details") {
|
|
46
|
+
path.append("details")
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
Button("Go Deep (3 levels)") {
|
|
50
|
+
path.append("level1")
|
|
51
|
+
path.append("level2")
|
|
52
|
+
path.append("level3")
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
.navigationTitle("Home")
|
|
56
|
+
.navigationDestination(for: String.self) { value in
|
|
57
|
+
DetailView(value: value, path: $path)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
struct DetailView: View {
|
|
64
|
+
let value: String
|
|
65
|
+
@Binding var path: NavigationPath
|
|
66
|
+
|
|
67
|
+
var body: some View {
|
|
68
|
+
VStack {
|
|
69
|
+
Text("Showing: \(value)")
|
|
70
|
+
|
|
71
|
+
Button("Pop to Root") {
|
|
72
|
+
path = NavigationPath()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**navigationDestination modifier:**
|
|
80
|
+
|
|
81
|
+
The navigationDestination modifier enables type-based routing. You can register multiple destination handlers for different data types:
|
|
82
|
+
|
|
83
|
+
```swift
|
|
84
|
+
struct MultiTypeNavigation: View {
|
|
85
|
+
@State private var path = NavigationPath()
|
|
86
|
+
|
|
87
|
+
var body: some View {
|
|
88
|
+
NavigationStack(path: $path) {
|
|
89
|
+
List {
|
|
90
|
+
Button("Show User") {
|
|
91
|
+
path.append(User(id: 1, name: "Alice"))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Button("Show Product") {
|
|
95
|
+
path.append(Product(id: 100, title: "iPhone"))
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
.navigationDestination(for: User.self) { user in
|
|
99
|
+
UserDetailView(user: user)
|
|
100
|
+
}
|
|
101
|
+
.navigationDestination(for: Product.self) { product in
|
|
102
|
+
ProductDetailView(product: product)
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
struct User: Hashable, Codable {
|
|
109
|
+
let id: Int
|
|
110
|
+
let name: String
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
struct Product: Hashable, Codable {
|
|
114
|
+
let id: Int
|
|
115
|
+
let title: String
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Key rules:**
|
|
120
|
+
- Place navigationDestination inside NavigationStack, not on child views
|
|
121
|
+
- Don't place navigationDestination on lazy containers (List, ScrollView, LazyVStack)
|
|
122
|
+
- Top-level navigationDestination always overrides lower ones for the same type
|
|
123
|
+
- Each destination type must be Hashable
|
|
124
|
+
|
|
125
|
+
**Navigation state in @Observable:**
|
|
126
|
+
```swift
|
|
127
|
+
import Observation
|
|
128
|
+
|
|
129
|
+
@Observable
|
|
130
|
+
class NavigationManager {
|
|
131
|
+
var path = NavigationPath()
|
|
132
|
+
|
|
133
|
+
func push(_ destination: Destination) {
|
|
134
|
+
path.append(destination)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
func pop() {
|
|
138
|
+
guard !path.isEmpty else { return }
|
|
139
|
+
path.removeLast()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
func popToRoot() {
|
|
143
|
+
path = NavigationPath()
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
enum Destination: Hashable {
|
|
148
|
+
case detail(id: Int)
|
|
149
|
+
case settings
|
|
150
|
+
case profile
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
struct AppView: View {
|
|
154
|
+
@State private var navigation = NavigationManager()
|
|
155
|
+
|
|
156
|
+
var body: some View {
|
|
157
|
+
@Bindable var nav = navigation
|
|
158
|
+
|
|
159
|
+
NavigationStack(path: $nav.path) {
|
|
160
|
+
List {
|
|
161
|
+
Button("Details") {
|
|
162
|
+
navigation.push(.detail(id: 1))
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
Button("Settings") {
|
|
166
|
+
navigation.push(.settings)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
.navigationDestination(for: Destination.self) { destination in
|
|
170
|
+
switch destination {
|
|
171
|
+
case .detail(let id):
|
|
172
|
+
DetailView(id: id, navigation: navigation)
|
|
173
|
+
case .settings:
|
|
174
|
+
SettingsView(navigation: navigation)
|
|
175
|
+
case .profile:
|
|
176
|
+
ProfileView(navigation: navigation)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
</navigation_stack>
|
|
184
|
+
|
|
185
|
+
<programmatic_navigation>
|
|
186
|
+
## Programmatic Navigation
|
|
187
|
+
|
|
188
|
+
NavigationPath provides programmatic control over the navigation stack without requiring NavigationLink user interaction.
|
|
189
|
+
|
|
190
|
+
**Push to path:**
|
|
191
|
+
```swift
|
|
192
|
+
// Push single destination
|
|
193
|
+
path.append(DetailDestination.item(id: 123))
|
|
194
|
+
|
|
195
|
+
// Push multiple levels at once
|
|
196
|
+
path.append(contentsOf: [screen1, screen2, screen3])
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
**Pop operations:**
|
|
200
|
+
```swift
|
|
201
|
+
// Pop one level
|
|
202
|
+
path.removeLast()
|
|
203
|
+
|
|
204
|
+
// Pop multiple levels
|
|
205
|
+
path.removeLast(2)
|
|
206
|
+
|
|
207
|
+
// Pop to root (clear entire stack)
|
|
208
|
+
path = NavigationPath()
|
|
209
|
+
|
|
210
|
+
// Conditional pop
|
|
211
|
+
if path.count > 0 {
|
|
212
|
+
path.removeLast()
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Deep navigation example:**
|
|
217
|
+
```swift
|
|
218
|
+
@Observable
|
|
219
|
+
class Router {
|
|
220
|
+
var path = NavigationPath()
|
|
221
|
+
|
|
222
|
+
func navigateToUserPosts(userId: Int, postId: Int) {
|
|
223
|
+
// Navigate through multiple screens
|
|
224
|
+
path.append(Route.userDetail(userId))
|
|
225
|
+
path.append(Route.userPosts(userId))
|
|
226
|
+
path.append(Route.postDetail(postId))
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
func popToUserDetail() {
|
|
230
|
+
// Remove specific number of levels
|
|
231
|
+
if path.count >= 2 {
|
|
232
|
+
path.removeLast(2)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
enum Route: Hashable {
|
|
238
|
+
case userDetail(Int)
|
|
239
|
+
case userPosts(Int)
|
|
240
|
+
case postDetail(Int)
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**NavigationPath count and inspection:**
|
|
245
|
+
```swift
|
|
246
|
+
struct NavigationDebugView: View {
|
|
247
|
+
@State private var path = NavigationPath()
|
|
248
|
+
|
|
249
|
+
var body: some View {
|
|
250
|
+
NavigationStack(path: $path) {
|
|
251
|
+
VStack {
|
|
252
|
+
Text("Stack depth: \(path.count)")
|
|
253
|
+
|
|
254
|
+
Button("Push") {
|
|
255
|
+
path.append("Level \(path.count + 1)")
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Button("Pop") {
|
|
259
|
+
if !path.isEmpty {
|
|
260
|
+
path.removeLast()
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
Button("Pop to Root") {
|
|
265
|
+
path = NavigationPath()
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
.navigationDestination(for: String.self) { value in
|
|
269
|
+
Text(value)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
```
|
|
275
|
+
</programmatic_navigation>
|
|
276
|
+
|
|
277
|
+
<sheets_and_covers>
|
|
278
|
+
## Sheet and FullScreenCover
|
|
279
|
+
|
|
280
|
+
Sheets present modal content on top of the current view. They are not part of the NavigationStack hierarchy.
|
|
281
|
+
|
|
282
|
+
**Basic sheet with boolean:**
|
|
283
|
+
```swift
|
|
284
|
+
struct SheetExample: View {
|
|
285
|
+
@State private var showingSheet = false
|
|
286
|
+
|
|
287
|
+
var body: some View {
|
|
288
|
+
Button("Show Sheet") {
|
|
289
|
+
showingSheet = true
|
|
290
|
+
}
|
|
291
|
+
.sheet(isPresented: $showingSheet) {
|
|
292
|
+
SheetContentView()
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
struct SheetContentView: View {
|
|
298
|
+
@Environment(\.dismiss) private var dismiss
|
|
299
|
+
|
|
300
|
+
var body: some View {
|
|
301
|
+
NavigationStack {
|
|
302
|
+
VStack {
|
|
303
|
+
Text("Sheet Content")
|
|
304
|
+
Button("Close") {
|
|
305
|
+
dismiss()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
.navigationTitle("Modal")
|
|
309
|
+
.toolbar {
|
|
310
|
+
ToolbarItem(placement: .cancellationAction) {
|
|
311
|
+
Button("Cancel") {
|
|
312
|
+
dismiss()
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
**Item-based presentation (type-safe, recommended):**
|
|
322
|
+
```swift
|
|
323
|
+
struct ItemSheet: View {
|
|
324
|
+
@State private var selectedUser: User?
|
|
325
|
+
|
|
326
|
+
var body: some View {
|
|
327
|
+
List(users) { user in
|
|
328
|
+
Button(user.name) {
|
|
329
|
+
selectedUser = user
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
.sheet(item: $selectedUser) { user in
|
|
333
|
+
UserDetailSheet(user: user)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
struct User: Identifiable {
|
|
339
|
+
let id: UUID
|
|
340
|
+
let name: String
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
struct UserDetailSheet: View {
|
|
344
|
+
let user: User
|
|
345
|
+
@Environment(\.dismiss) private var dismiss
|
|
346
|
+
|
|
347
|
+
var body: some View {
|
|
348
|
+
NavigationStack {
|
|
349
|
+
VStack {
|
|
350
|
+
Text("User: \(user.name)")
|
|
351
|
+
}
|
|
352
|
+
.navigationTitle(user.name)
|
|
353
|
+
.toolbar {
|
|
354
|
+
ToolbarItem(placement: .confirmationAction) {
|
|
355
|
+
Button("Done") {
|
|
356
|
+
dismiss()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
**FullScreenCover:**
|
|
366
|
+
```swift
|
|
367
|
+
struct FullScreenExample: View {
|
|
368
|
+
@State private var showingFullScreen = false
|
|
369
|
+
|
|
370
|
+
var body: some View {
|
|
371
|
+
Button("Show Full Screen") {
|
|
372
|
+
showingFullScreen = true
|
|
373
|
+
}
|
|
374
|
+
.fullScreenCover(isPresented: $showingFullScreen) {
|
|
375
|
+
FullScreenContentView()
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
struct FullScreenContentView: View {
|
|
381
|
+
@Environment(\.dismiss) private var dismiss
|
|
382
|
+
|
|
383
|
+
var body: some View {
|
|
384
|
+
NavigationStack {
|
|
385
|
+
VStack {
|
|
386
|
+
Text("Full Screen Content")
|
|
387
|
+
.font(.largeTitle)
|
|
388
|
+
}
|
|
389
|
+
.toolbar {
|
|
390
|
+
ToolbarItem(placement: .cancellationAction) {
|
|
391
|
+
Button("Close") {
|
|
392
|
+
dismiss()
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
**Presentation detents (iOS 16+):**
|
|
402
|
+
```swift
|
|
403
|
+
struct DetentSheet: View {
|
|
404
|
+
@State private var showingSheet = false
|
|
405
|
+
@State private var selectedDetent: PresentationDetent = .medium
|
|
406
|
+
|
|
407
|
+
var body: some View {
|
|
408
|
+
Button("Show Customizable Sheet") {
|
|
409
|
+
showingSheet = true
|
|
410
|
+
}
|
|
411
|
+
.sheet(isPresented: $showingSheet) {
|
|
412
|
+
SheetWithDetents(selectedDetent: $selectedDetent)
|
|
413
|
+
.presentationDetents(
|
|
414
|
+
[.medium, .large, .fraction(0.25), .height(200)],
|
|
415
|
+
selection: $selectedDetent
|
|
416
|
+
)
|
|
417
|
+
.presentationDragIndicator(.visible)
|
|
418
|
+
.presentationBackgroundInteraction(.enabled(upThrough: .medium))
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
struct SheetWithDetents: View {
|
|
424
|
+
@Binding var selectedDetent: PresentationDetent
|
|
425
|
+
|
|
426
|
+
var body: some View {
|
|
427
|
+
VStack {
|
|
428
|
+
Text("Drag to resize")
|
|
429
|
+
.font(.headline)
|
|
430
|
+
|
|
431
|
+
Text("Current detent: \(detentDescription)")
|
|
432
|
+
.font(.caption)
|
|
433
|
+
}
|
|
434
|
+
.padding()
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
var detentDescription: String {
|
|
438
|
+
if selectedDetent == .medium { return "Medium" }
|
|
439
|
+
if selectedDetent == .large { return "Large" }
|
|
440
|
+
return "Custom"
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**Dismiss from presented view:**
|
|
446
|
+
```swift
|
|
447
|
+
struct DismissExample: View {
|
|
448
|
+
@Environment(\.dismiss) private var dismiss
|
|
449
|
+
|
|
450
|
+
var body: some View {
|
|
451
|
+
VStack {
|
|
452
|
+
Text("Modal Content")
|
|
453
|
+
|
|
454
|
+
Button("Dismiss") {
|
|
455
|
+
dismiss()
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
```
|
|
461
|
+
</sheets_and_covers>
|
|
462
|
+
|
|
463
|
+
<tab_view>
|
|
464
|
+
## TabView
|
|
465
|
+
|
|
466
|
+
TabView presents multiple independent navigation hierarchies. Each tab typically contains its own NavigationStack.
|
|
467
|
+
|
|
468
|
+
**Basic TabView:**
|
|
469
|
+
```swift
|
|
470
|
+
struct TabExample: View {
|
|
471
|
+
var body: some View {
|
|
472
|
+
TabView {
|
|
473
|
+
HomeView()
|
|
474
|
+
.tabItem {
|
|
475
|
+
Label("Home", systemImage: "house")
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
SearchView()
|
|
479
|
+
.tabItem {
|
|
480
|
+
Label("Search", systemImage: "magnifyingglass")
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
ProfileView()
|
|
484
|
+
.tabItem {
|
|
485
|
+
Label("Profile", systemImage: "person")
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
struct HomeView: View {
|
|
492
|
+
var body: some View {
|
|
493
|
+
NavigationStack {
|
|
494
|
+
List {
|
|
495
|
+
Text("Home Content")
|
|
496
|
+
}
|
|
497
|
+
.navigationTitle("Home")
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Programmatic tab selection:**
|
|
504
|
+
```swift
|
|
505
|
+
struct ProgrammaticTabView: View {
|
|
506
|
+
@State private var selectedTab = Tab.home
|
|
507
|
+
|
|
508
|
+
var body: some View {
|
|
509
|
+
TabView(selection: $selectedTab) {
|
|
510
|
+
HomeView()
|
|
511
|
+
.tabItem {
|
|
512
|
+
Label("Home", systemImage: "house")
|
|
513
|
+
}
|
|
514
|
+
.tag(Tab.home)
|
|
515
|
+
|
|
516
|
+
SearchView()
|
|
517
|
+
.tabItem {
|
|
518
|
+
Label("Search", systemImage: "magnifyingglass")
|
|
519
|
+
}
|
|
520
|
+
.tag(Tab.search)
|
|
521
|
+
|
|
522
|
+
ProfileView()
|
|
523
|
+
.tabItem {
|
|
524
|
+
Label("Profile", systemImage: "person")
|
|
525
|
+
}
|
|
526
|
+
.tag(Tab.profile)
|
|
527
|
+
}
|
|
528
|
+
.onChange(of: selectedTab) { oldValue, newValue in
|
|
529
|
+
print("Tab changed from \(oldValue) to \(newValue)")
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
enum Tab {
|
|
535
|
+
case home
|
|
536
|
+
case search
|
|
537
|
+
case profile
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
**Each tab with independent NavigationStack:**
|
|
542
|
+
```swift
|
|
543
|
+
struct IndependentTabStacks: View {
|
|
544
|
+
@State private var homeNavPath = NavigationPath()
|
|
545
|
+
@State private var searchNavPath = NavigationPath()
|
|
546
|
+
|
|
547
|
+
var body: some View {
|
|
548
|
+
TabView {
|
|
549
|
+
NavigationStack(path: $homeNavPath) {
|
|
550
|
+
HomeRootView()
|
|
551
|
+
.navigationDestination(for: HomeDestination.self) { destination in
|
|
552
|
+
// Home-specific destinations
|
|
553
|
+
Text("Home destination")
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
.tabItem {
|
|
557
|
+
Label("Home", systemImage: "house")
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
NavigationStack(path: $searchNavPath) {
|
|
561
|
+
SearchRootView()
|
|
562
|
+
.navigationDestination(for: SearchDestination.self) { destination in
|
|
563
|
+
// Search-specific destinations
|
|
564
|
+
Text("Search destination")
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
.tabItem {
|
|
568
|
+
Label("Search", systemImage: "magnifyingglass")
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
enum HomeDestination: Hashable {
|
|
575
|
+
case detail(Int)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
enum SearchDestination: Hashable {
|
|
579
|
+
case results(String)
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
**iOS 18 Tab API:**
|
|
584
|
+
```swift
|
|
585
|
+
// iOS 18 introduces new Tab syntax with better customization
|
|
586
|
+
@available(iOS 18.0, *)
|
|
587
|
+
struct ModernTabView: View {
|
|
588
|
+
@State private var selectedTab: TabIdentifier = .home
|
|
589
|
+
|
|
590
|
+
var body: some View {
|
|
591
|
+
TabView(selection: $selectedTab) {
|
|
592
|
+
Tab("Home", systemImage: "house", value: .home) {
|
|
593
|
+
NavigationStack {
|
|
594
|
+
HomeView()
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
Tab("Search", systemImage: "magnifyingglass", value: .search) {
|
|
599
|
+
NavigationStack {
|
|
600
|
+
SearchView()
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
.badge(5) // Badge support
|
|
604
|
+
|
|
605
|
+
Tab("Profile", systemImage: "person", value: .profile) {
|
|
606
|
+
NavigationStack {
|
|
607
|
+
ProfileView()
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
.customizationID("profile") // Enables tab customization
|
|
611
|
+
}
|
|
612
|
+
.tabViewStyle(.sidebarAdaptable) // Sidebar on iPad
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
enum TabIdentifier: Hashable {
|
|
617
|
+
case home
|
|
618
|
+
case search
|
|
619
|
+
case profile
|
|
620
|
+
}
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
**Tab badges:**
|
|
624
|
+
```swift
|
|
625
|
+
struct BadgedTabs: View {
|
|
626
|
+
@State private var unreadCount = 3
|
|
627
|
+
|
|
628
|
+
var body: some View {
|
|
629
|
+
TabView {
|
|
630
|
+
HomeView()
|
|
631
|
+
.tabItem {
|
|
632
|
+
Label("Home", systemImage: "house")
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
MessagesView()
|
|
636
|
+
.tabItem {
|
|
637
|
+
Label("Messages", systemImage: "message")
|
|
638
|
+
}
|
|
639
|
+
.badge(unreadCount)
|
|
640
|
+
|
|
641
|
+
ProfileView()
|
|
642
|
+
.tabItem {
|
|
643
|
+
Label("Profile", systemImage: "person")
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Platform differences:**
|
|
651
|
+
- **iOS:** Bottom tabs with up to 5 visible items (more creates "More" tab)
|
|
652
|
+
- **macOS:** Top tabs or sidebar style
|
|
653
|
+
- **iPadOS:** Can transform to sidebar with .tabViewStyle(.sidebarAdaptable)
|
|
654
|
+
- **watchOS:** PageTabViewStyle (swipeable pages)
|
|
655
|
+
</tab_view>
|
|
656
|
+
|
|
657
|
+
<deep_linking>
|
|
658
|
+
## Deep Linking
|
|
659
|
+
|
|
660
|
+
Deep linking enables opening your app to specific screens via URLs, supporting both custom URL schemes and Universal Links.
|
|
661
|
+
|
|
662
|
+
**URL handling with onOpenURL:**
|
|
663
|
+
```swift
|
|
664
|
+
@main
|
|
665
|
+
struct MyApp: App {
|
|
666
|
+
@State private var router = Router()
|
|
667
|
+
|
|
668
|
+
var body: some Scene {
|
|
669
|
+
WindowGroup {
|
|
670
|
+
ContentView()
|
|
671
|
+
.environment(router)
|
|
672
|
+
.onOpenURL { url in
|
|
673
|
+
handleDeepLink(url)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
private func handleDeepLink(_ url: URL) {
|
|
679
|
+
router.handleDeepLink(url)
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
@Observable
|
|
684
|
+
class Router {
|
|
685
|
+
var path = NavigationPath()
|
|
686
|
+
|
|
687
|
+
func handleDeepLink(_ url: URL) {
|
|
688
|
+
// Parse URL and update navigation
|
|
689
|
+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// myapp://user/123
|
|
694
|
+
if components.scheme == "myapp",
|
|
695
|
+
components.host == "user",
|
|
696
|
+
let userId = components.path.split(separator: "/").first,
|
|
697
|
+
let id = Int(userId) {
|
|
698
|
+
navigateToUser(id: id)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// myapp://product/456/reviews
|
|
702
|
+
if components.scheme == "myapp",
|
|
703
|
+
components.host == "product" {
|
|
704
|
+
let pathComponents = components.path.split(separator: "/")
|
|
705
|
+
if let productId = pathComponents.first,
|
|
706
|
+
let id = Int(productId) {
|
|
707
|
+
navigateToProduct(id: id, showReviews: pathComponents.contains("reviews"))
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
func navigateToUser(id: Int) {
|
|
713
|
+
path = NavigationPath() // Reset to root
|
|
714
|
+
path.append(Route.userDetail(id))
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
func navigateToProduct(id: Int, showReviews: Bool) {
|
|
718
|
+
path = NavigationPath()
|
|
719
|
+
path.append(Route.productDetail(id))
|
|
720
|
+
if showReviews {
|
|
721
|
+
path.append(Route.productReviews(id))
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
enum Route: Hashable {
|
|
727
|
+
case userDetail(Int)
|
|
728
|
+
case productDetail(Int)
|
|
729
|
+
case productReviews(Int)
|
|
730
|
+
}
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
**Parsing URLs into navigation state:**
|
|
734
|
+
```swift
|
|
735
|
+
@Observable
|
|
736
|
+
class DeepLinkRouter {
|
|
737
|
+
var path = NavigationPath()
|
|
738
|
+
var selectedTab: AppTab = .home
|
|
739
|
+
|
|
740
|
+
func handleDeepLink(_ url: URL) {
|
|
741
|
+
// Parse URL: myapp://tab/search?query=SwiftUI&filter=recent
|
|
742
|
+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
|
743
|
+
return
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Handle tab switching
|
|
747
|
+
if components.host == "tab",
|
|
748
|
+
let tabName = components.path.split(separator: "/").first {
|
|
749
|
+
switchTab(String(tabName))
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Handle query parameters
|
|
753
|
+
let queryItems = components.queryItems ?? []
|
|
754
|
+
if let query = queryItems.first(where: { $0.name == "query" })?.value {
|
|
755
|
+
navigateToSearch(query: query)
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
private func switchTab(_ tab: String) {
|
|
760
|
+
switch tab {
|
|
761
|
+
case "home": selectedTab = .home
|
|
762
|
+
case "search": selectedTab = .search
|
|
763
|
+
case "profile": selectedTab = .profile
|
|
764
|
+
default: break
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
private func navigateToSearch(query: String) {
|
|
769
|
+
selectedTab = .search
|
|
770
|
+
path = NavigationPath()
|
|
771
|
+
path.append(SearchRoute.results(query))
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
enum AppTab {
|
|
776
|
+
case home
|
|
777
|
+
case search
|
|
778
|
+
case profile
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
enum SearchRoute: Hashable {
|
|
782
|
+
case results(String)
|
|
783
|
+
}
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
**Universal Links setup:**
|
|
787
|
+
|
|
788
|
+
1. **Associated Domains entitlement:** Add in Xcode project capabilities
|
|
789
|
+
- `applinks:example.com`
|
|
790
|
+
|
|
791
|
+
2. **apple-app-site-association file:** Host at `https://example.com/.well-known/apple-app-site-association`
|
|
792
|
+
```json
|
|
793
|
+
{
|
|
794
|
+
"applinks": {
|
|
795
|
+
"apps": [],
|
|
796
|
+
"details": [
|
|
797
|
+
{
|
|
798
|
+
"appID": "TEAM_ID.com.example.myapp",
|
|
799
|
+
"paths": [
|
|
800
|
+
"/user/*",
|
|
801
|
+
"/product/*"
|
|
802
|
+
]
|
|
803
|
+
}
|
|
804
|
+
]
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
3. **Handle in app:**
|
|
810
|
+
```swift
|
|
811
|
+
@main
|
|
812
|
+
struct MyApp: App {
|
|
813
|
+
@State private var router = Router()
|
|
814
|
+
|
|
815
|
+
var body: some Scene {
|
|
816
|
+
WindowGroup {
|
|
817
|
+
ContentView()
|
|
818
|
+
.environment(router)
|
|
819
|
+
.onOpenURL { url in
|
|
820
|
+
router.handleUniversalLink(url)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
**Custom URL schemes:**
|
|
828
|
+
|
|
829
|
+
1. **Register scheme in Info.plist:**
|
|
830
|
+
```xml
|
|
831
|
+
<key>CFBundleURLTypes</key>
|
|
832
|
+
<array>
|
|
833
|
+
<dict>
|
|
834
|
+
<key>CFBundleURLSchemes</key>
|
|
835
|
+
<array>
|
|
836
|
+
<string>myapp</string>
|
|
837
|
+
</array>
|
|
838
|
+
<key>CFBundleURLName</key>
|
|
839
|
+
<string>com.example.myapp</string>
|
|
840
|
+
</dict>
|
|
841
|
+
</array>
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
2. **Handle custom scheme:**
|
|
845
|
+
```swift
|
|
846
|
+
struct ContentView: View {
|
|
847
|
+
@Environment(Router.self) private var router
|
|
848
|
+
|
|
849
|
+
var body: some View {
|
|
850
|
+
@Bindable var router = router
|
|
851
|
+
|
|
852
|
+
NavigationStack(path: $router.path) {
|
|
853
|
+
HomeView()
|
|
854
|
+
.navigationDestination(for: Route.self) { route in
|
|
855
|
+
destinationView(for: route)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
@ViewBuilder
|
|
861
|
+
func destinationView(for route: Route) -> some View {
|
|
862
|
+
switch route {
|
|
863
|
+
case .userDetail(let id):
|
|
864
|
+
UserDetailView(userId: id)
|
|
865
|
+
case .productDetail(let id):
|
|
866
|
+
ProductDetailView(productId: id)
|
|
867
|
+
case .productReviews(let id):
|
|
868
|
+
ProductReviewsView(productId: id)
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
**Security considerations:**
|
|
875
|
+
- Validate all incoming URLs
|
|
876
|
+
- Sanitize parameters before using them
|
|
877
|
+
- Don't expose sensitive functionality via deep links
|
|
878
|
+
- Use Universal Links over custom URL schemes for production (more secure, unique)
|
|
879
|
+
</deep_linking>
|
|
880
|
+
|
|
881
|
+
<coordinator_pattern>
|
|
882
|
+
## Coordinator Pattern (Optional)
|
|
883
|
+
|
|
884
|
+
The Coordinator pattern centralizes navigation logic, decoupling it from views. Use when navigation becomes complex enough to justify the abstraction.
|
|
885
|
+
|
|
886
|
+
**When to use:**
|
|
887
|
+
- Complex navigation flows with many paths
|
|
888
|
+
- Testable navigation logic separated from views
|
|
889
|
+
- Multiple entry points to the same flow
|
|
890
|
+
- Deep linking with complex routing
|
|
891
|
+
|
|
892
|
+
**Implementation with @Observable:**
|
|
893
|
+
```swift
|
|
894
|
+
import Observation
|
|
895
|
+
|
|
896
|
+
@Observable
|
|
897
|
+
class AppCoordinator {
|
|
898
|
+
var path = NavigationPath()
|
|
899
|
+
var sheet: Sheet?
|
|
900
|
+
var fullScreenCover: Cover?
|
|
901
|
+
|
|
902
|
+
// MARK: - Navigation
|
|
903
|
+
|
|
904
|
+
func push(_ destination: Destination) {
|
|
905
|
+
path.append(destination)
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
func pop() {
|
|
909
|
+
guard !path.isEmpty else { return }
|
|
910
|
+
path.removeLast()
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
func popToRoot() {
|
|
914
|
+
path = NavigationPath()
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
// MARK: - Sheets
|
|
918
|
+
|
|
919
|
+
func presentSheet(_ sheet: Sheet) {
|
|
920
|
+
self.sheet = sheet
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
func dismissSheet() {
|
|
924
|
+
self.sheet = nil
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// MARK: - Full Screen
|
|
928
|
+
|
|
929
|
+
func presentFullScreen(_ cover: Cover) {
|
|
930
|
+
self.fullScreenCover = cover
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
func dismissFullScreen() {
|
|
934
|
+
self.fullScreenCover = nil
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
// MARK: - Deep Linking
|
|
938
|
+
|
|
939
|
+
func handleDeepLink(_ url: URL) {
|
|
940
|
+
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else {
|
|
941
|
+
return
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if components.path.contains("/user/"),
|
|
945
|
+
let userId = extractId(from: components.path) {
|
|
946
|
+
popToRoot()
|
|
947
|
+
push(.userDetail(userId))
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private func extractId(from path: String) -> Int? {
|
|
952
|
+
let components = path.split(separator: "/")
|
|
953
|
+
return components.last.flatMap { Int($0) }
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
enum Destination: Hashable {
|
|
958
|
+
case userDetail(Int)
|
|
959
|
+
case settings
|
|
960
|
+
case editProfile
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
enum Sheet: Identifiable {
|
|
964
|
+
case addUser
|
|
965
|
+
case filter
|
|
966
|
+
|
|
967
|
+
var id: String {
|
|
968
|
+
switch self {
|
|
969
|
+
case .addUser: return "addUser"
|
|
970
|
+
case .filter: return "filter"
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
enum Cover: Identifiable {
|
|
976
|
+
case onboarding
|
|
977
|
+
case camera
|
|
978
|
+
|
|
979
|
+
var id: String {
|
|
980
|
+
switch self {
|
|
981
|
+
case .onboarding: return "onboarding"
|
|
982
|
+
case .camera: return "camera"
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// MARK: - Root View
|
|
988
|
+
|
|
989
|
+
struct RootView: View {
|
|
990
|
+
@State private var coordinator = AppCoordinator()
|
|
991
|
+
|
|
992
|
+
var body: some View {
|
|
993
|
+
@Bindable var coordinator = coordinator
|
|
994
|
+
|
|
995
|
+
NavigationStack(path: $coordinator.path) {
|
|
996
|
+
UserListView()
|
|
997
|
+
.navigationDestination(for: Destination.self) { destination in
|
|
998
|
+
destinationView(for: destination)
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
.sheet(item: $coordinator.sheet) { sheet in
|
|
1002
|
+
sheetView(for: sheet)
|
|
1003
|
+
}
|
|
1004
|
+
.fullScreenCover(item: $coordinator.fullScreenCover) { cover in
|
|
1005
|
+
coverView(for: cover)
|
|
1006
|
+
}
|
|
1007
|
+
.environment(coordinator)
|
|
1008
|
+
.onOpenURL { url in
|
|
1009
|
+
coordinator.handleDeepLink(url)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
@ViewBuilder
|
|
1014
|
+
func destinationView(for destination: Destination) -> some View {
|
|
1015
|
+
switch destination {
|
|
1016
|
+
case .userDetail(let id):
|
|
1017
|
+
UserDetailView(userId: id)
|
|
1018
|
+
case .settings:
|
|
1019
|
+
SettingsView()
|
|
1020
|
+
case .editProfile:
|
|
1021
|
+
EditProfileView()
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
@ViewBuilder
|
|
1026
|
+
func sheetView(for sheet: Sheet) -> some View {
|
|
1027
|
+
switch sheet {
|
|
1028
|
+
case .addUser:
|
|
1029
|
+
AddUserView()
|
|
1030
|
+
case .filter:
|
|
1031
|
+
FilterView()
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
@ViewBuilder
|
|
1036
|
+
func coverView(for cover: Cover) -> some View {
|
|
1037
|
+
switch cover {
|
|
1038
|
+
case .onboarding:
|
|
1039
|
+
OnboardingView()
|
|
1040
|
+
case .camera:
|
|
1041
|
+
CameraView()
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// MARK: - Views using coordinator
|
|
1047
|
+
|
|
1048
|
+
struct UserListView: View {
|
|
1049
|
+
@Environment(AppCoordinator.self) private var coordinator
|
|
1050
|
+
|
|
1051
|
+
var body: some View {
|
|
1052
|
+
List {
|
|
1053
|
+
ForEach(users) { user in
|
|
1054
|
+
Button(user.name) {
|
|
1055
|
+
coordinator.push(.userDetail(user.id))
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
.navigationTitle("Users")
|
|
1060
|
+
.toolbar {
|
|
1061
|
+
Button("Add") {
|
|
1062
|
+
coordinator.presentSheet(.addUser)
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
let users = [
|
|
1068
|
+
User(id: 1, name: "Alice"),
|
|
1069
|
+
User(id: 2, name: "Bob")
|
|
1070
|
+
]
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
struct UserDetailView: View {
|
|
1074
|
+
let userId: Int
|
|
1075
|
+
@Environment(AppCoordinator.self) private var coordinator
|
|
1076
|
+
|
|
1077
|
+
var body: some View {
|
|
1078
|
+
VStack {
|
|
1079
|
+
Text("User \(userId)")
|
|
1080
|
+
|
|
1081
|
+
Button("Edit Profile") {
|
|
1082
|
+
coordinator.push(.editProfile)
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
Button("Pop to Root") {
|
|
1086
|
+
coordinator.popToRoot()
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
.navigationTitle("User Detail")
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
**Trade-offs:**
|
|
1095
|
+
- **Pros:** Testable navigation logic, centralized flow control, easier deep linking, decoupled views
|
|
1096
|
+
- **Cons:** Additional abstraction layer, more code to maintain, can be overkill for simple apps
|
|
1097
|
+
|
|
1098
|
+
**When NOT to use:** Simple apps with linear navigation, apps with fewer than 10 screens, prototypes
|
|
1099
|
+
</coordinator_pattern>
|
|
1100
|
+
|
|
1101
|
+
<state_persistence>
|
|
1102
|
+
## Navigation State Persistence
|
|
1103
|
+
|
|
1104
|
+
Enable state restoration so users return to where they left off when reopening your app.
|
|
1105
|
+
|
|
1106
|
+
**Codable NavigationPath:**
|
|
1107
|
+
```swift
|
|
1108
|
+
struct PersistentNavigation: View {
|
|
1109
|
+
@State private var path = NavigationPath()
|
|
1110
|
+
@AppStorage("navigationPath") private var navigationPathData: Data?
|
|
1111
|
+
|
|
1112
|
+
var body: some View {
|
|
1113
|
+
NavigationStack(path: $path) {
|
|
1114
|
+
List {
|
|
1115
|
+
NavigationLink("Details", value: Route.details)
|
|
1116
|
+
NavigationLink("Settings", value: Route.settings)
|
|
1117
|
+
}
|
|
1118
|
+
.navigationTitle("Home")
|
|
1119
|
+
.navigationDestination(for: Route.self) { route in
|
|
1120
|
+
routeView(for: route)
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
.onAppear {
|
|
1124
|
+
restorePath()
|
|
1125
|
+
}
|
|
1126
|
+
.onChange(of: path) { oldPath, newPath in
|
|
1127
|
+
savePath()
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
@ViewBuilder
|
|
1132
|
+
func routeView(for route: Route) -> some View {
|
|
1133
|
+
switch route {
|
|
1134
|
+
case .details:
|
|
1135
|
+
Text("Details")
|
|
1136
|
+
case .settings:
|
|
1137
|
+
Text("Settings")
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
func savePath() {
|
|
1142
|
+
guard let representation = path.codable else { return }
|
|
1143
|
+
|
|
1144
|
+
do {
|
|
1145
|
+
let data = try JSONEncoder().encode(representation)
|
|
1146
|
+
navigationPathData = data
|
|
1147
|
+
} catch {
|
|
1148
|
+
print("Failed to save path: \(error)")
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
func restorePath() {
|
|
1153
|
+
guard let data = navigationPathData else { return }
|
|
1154
|
+
|
|
1155
|
+
do {
|
|
1156
|
+
let representation = try JSONDecoder().decode(
|
|
1157
|
+
NavigationPath.CodableRepresentation.self,
|
|
1158
|
+
from: data
|
|
1159
|
+
)
|
|
1160
|
+
path = NavigationPath(representation)
|
|
1161
|
+
} catch {
|
|
1162
|
+
print("Failed to restore path: \(error)")
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
enum Route: Hashable, Codable {
|
|
1168
|
+
case details
|
|
1169
|
+
case settings
|
|
1170
|
+
}
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
**@SceneStorage for restoration:**
|
|
1174
|
+
```swift
|
|
1175
|
+
struct SceneStorageNavigation: View {
|
|
1176
|
+
@SceneStorage("navigationPath") private var pathData: Data?
|
|
1177
|
+
@State private var path = NavigationPath()
|
|
1178
|
+
|
|
1179
|
+
var body: some View {
|
|
1180
|
+
NavigationStack(path: $path) {
|
|
1181
|
+
List {
|
|
1182
|
+
NavigationLink("Item 1", value: 1)
|
|
1183
|
+
NavigationLink("Item 2", value: 2)
|
|
1184
|
+
}
|
|
1185
|
+
.navigationDestination(for: Int.self) { value in
|
|
1186
|
+
DetailView(value: value)
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
.task {
|
|
1190
|
+
if let data = pathData,
|
|
1191
|
+
let representation = try? JSONDecoder().decode(
|
|
1192
|
+
NavigationPath.CodableRepresentation.self,
|
|
1193
|
+
from: data
|
|
1194
|
+
) {
|
|
1195
|
+
path = NavigationPath(representation)
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
.onChange(of: path) { _, newPath in
|
|
1199
|
+
if let representation = newPath.codable,
|
|
1200
|
+
let data = try? JSONEncoder().encode(representation) {
|
|
1201
|
+
pathData = data
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
**Important notes:**
|
|
1209
|
+
- Only works if all types in NavigationPath are Codable
|
|
1210
|
+
- @SceneStorage cleared when user force-quits app
|
|
1211
|
+
- @AppStorage persists across launches but not recommended for large data
|
|
1212
|
+
- Test restoration thoroughly (background app, force quit, etc.)
|
|
1213
|
+
</state_persistence>
|
|
1214
|
+
|
|
1215
|
+
<decision_tree>
|
|
1216
|
+
## Choosing the Right Approach
|
|
1217
|
+
|
|
1218
|
+
**Simple app with few screens:** NavigationStack with NavigationLink (user-driven navigation is sufficient)
|
|
1219
|
+
|
|
1220
|
+
**Need programmatic navigation:** NavigationStack + NavigationPath in @Observable class stored in @State
|
|
1221
|
+
|
|
1222
|
+
**Modal content (settings, forms, detail overlays):** .sheet() for dismissible modals, .fullScreenCover() for immersive content
|
|
1223
|
+
|
|
1224
|
+
**Multiple independent sections:** TabView with separate NavigationStack per tab
|
|
1225
|
+
|
|
1226
|
+
**Deep linking required:** onOpenURL + NavigationPath (parse URL and manipulate path programmatically)
|
|
1227
|
+
|
|
1228
|
+
**Complex navigation flows (10+ screens, multiple entry points):** Coordinator pattern with @Observable coordinator managing NavigationPath and sheet/cover state
|
|
1229
|
+
|
|
1230
|
+
**State restoration needed:** NavigationPath.codable with @SceneStorage or @AppStorage
|
|
1231
|
+
|
|
1232
|
+
**Platform differences matter:** Check platform in architecture.md, use NavigationSplitView for iPad/macOS multi-column layouts
|
|
1233
|
+
</decision_tree>
|
|
1234
|
+
|
|
1235
|
+
<anti_patterns>
|
|
1236
|
+
## What NOT to Do
|
|
1237
|
+
|
|
1238
|
+
<anti_pattern name="Using NavigationView">
|
|
1239
|
+
**Problem:** NavigationView is deprecated in iOS 16+
|
|
1240
|
+
|
|
1241
|
+
**Why it's bad:**
|
|
1242
|
+
- Missing modern features (programmatic navigation, type-safe routing)
|
|
1243
|
+
- Deprecated API that may be removed
|
|
1244
|
+
- NavigationStack is more performant and flexible
|
|
1245
|
+
|
|
1246
|
+
**Instead:** Use NavigationStack
|
|
1247
|
+
```swift
|
|
1248
|
+
// WRONG
|
|
1249
|
+
NavigationView {
|
|
1250
|
+
List { }
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// RIGHT
|
|
1254
|
+
NavigationStack {
|
|
1255
|
+
List { }
|
|
1256
|
+
}
|
|
1257
|
+
```
|
|
1258
|
+
</anti_pattern>
|
|
1259
|
+
|
|
1260
|
+
<anti_pattern name="Boolean flags for navigation">
|
|
1261
|
+
**Problem:** Using @State var showDetail = false for each destination
|
|
1262
|
+
|
|
1263
|
+
**Why it's bad:**
|
|
1264
|
+
- Doesn't scale beyond 2-3 screens
|
|
1265
|
+
- Loses type safety (what data does the destination need?)
|
|
1266
|
+
- Can't programmatically navigate deep
|
|
1267
|
+
- No navigation history
|
|
1268
|
+
|
|
1269
|
+
**Instead:** Use navigationDestination with typed values
|
|
1270
|
+
```swift
|
|
1271
|
+
// WRONG
|
|
1272
|
+
@State private var showUserDetail = false
|
|
1273
|
+
@State private var showSettings = false
|
|
1274
|
+
@State private var showProfile = false
|
|
1275
|
+
|
|
1276
|
+
// RIGHT
|
|
1277
|
+
@State private var path = NavigationPath()
|
|
1278
|
+
|
|
1279
|
+
NavigationStack(path: $path) {
|
|
1280
|
+
Button("Show User") {
|
|
1281
|
+
path.append(Route.userDetail(id: 1))
|
|
1282
|
+
}
|
|
1283
|
+
.navigationDestination(for: Route.self) { route in
|
|
1284
|
+
// Handle route
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
```
|
|
1288
|
+
</anti_pattern>
|
|
1289
|
+
|
|
1290
|
+
<anti_pattern name="Storing NavigationPath in @State at wrong level">
|
|
1291
|
+
**Problem:** Storing NavigationPath in child views that need to access it
|
|
1292
|
+
|
|
1293
|
+
**Why it's bad:**
|
|
1294
|
+
- Child views can't access parent's NavigationPath
|
|
1295
|
+
- Forces passing bindings through many levels
|
|
1296
|
+
- Breaks encapsulation
|
|
1297
|
+
|
|
1298
|
+
**Instead:** Store in @Observable, pass via @Environment
|
|
1299
|
+
```swift
|
|
1300
|
+
// WRONG
|
|
1301
|
+
struct ChildView: View {
|
|
1302
|
+
@State private var path = NavigationPath() // Can't access parent's path
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
// RIGHT
|
|
1306
|
+
@Observable
|
|
1307
|
+
class Router {
|
|
1308
|
+
var path = NavigationPath()
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
@main
|
|
1312
|
+
struct App: App {
|
|
1313
|
+
@State private var router = Router()
|
|
1314
|
+
|
|
1315
|
+
var body: some Scene {
|
|
1316
|
+
WindowGroup {
|
|
1317
|
+
ContentView()
|
|
1318
|
+
.environment(router)
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
struct ChildView: View {
|
|
1324
|
+
@Environment(Router.self) private var router
|
|
1325
|
+
|
|
1326
|
+
var body: some View {
|
|
1327
|
+
Button("Navigate") {
|
|
1328
|
+
router.path.append(destination)
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
```
|
|
1333
|
+
</anti_pattern>
|
|
1334
|
+
|
|
1335
|
+
<anti_pattern name="Placing navigationDestination on lazy containers">
|
|
1336
|
+
**Problem:** Putting navigationDestination inside List, ScrollView, LazyVStack
|
|
1337
|
+
|
|
1338
|
+
**Why it's bad:**
|
|
1339
|
+
- Destination closures may not be called
|
|
1340
|
+
- Lazy loading means modifiers aren't registered
|
|
1341
|
+
- Apple explicitly warns against this
|
|
1342
|
+
|
|
1343
|
+
**Instead:** Place navigationDestination on NavigationStack or its immediate child
|
|
1344
|
+
```swift
|
|
1345
|
+
// WRONG
|
|
1346
|
+
NavigationStack {
|
|
1347
|
+
List {
|
|
1348
|
+
ForEach(items) { item in
|
|
1349
|
+
NavigationLink(item.name, value: item)
|
|
1350
|
+
}
|
|
1351
|
+
.navigationDestination(for: Item.self) { item in // ❌ Inside List
|
|
1352
|
+
DetailView(item: item)
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// RIGHT
|
|
1358
|
+
NavigationStack {
|
|
1359
|
+
List {
|
|
1360
|
+
ForEach(items) { item in
|
|
1361
|
+
NavigationLink(item.name, value: item)
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
.navigationDestination(for: Item.self) { item in // ✅ Outside List
|
|
1365
|
+
DetailView(item: item)
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
```
|
|
1369
|
+
</anti_pattern>
|
|
1370
|
+
|
|
1371
|
+
<anti_pattern name="Mixing sheets with NavigationStack for sequential flows">
|
|
1372
|
+
**Problem:** Using sheets for multi-step flows that should be pushed
|
|
1373
|
+
|
|
1374
|
+
**Why it's bad:**
|
|
1375
|
+
- Sheets are for modal content, not hierarchical navigation
|
|
1376
|
+
- Can't use back button (must dismiss)
|
|
1377
|
+
- Breaks user expectations
|
|
1378
|
+
- No navigation history
|
|
1379
|
+
|
|
1380
|
+
**Instead:** Use NavigationStack for flows, sheets for modals
|
|
1381
|
+
```swift
|
|
1382
|
+
// WRONG - using sheets for sequential steps
|
|
1383
|
+
.sheet(isPresented: $showStep2) {
|
|
1384
|
+
Step2View()
|
|
1385
|
+
.sheet(isPresented: $showStep3) {
|
|
1386
|
+
Step3View() // Nested sheets
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// RIGHT - NavigationStack for flows
|
|
1391
|
+
NavigationStack(path: $path) {
|
|
1392
|
+
Step1View()
|
|
1393
|
+
.navigationDestination(for: Step.self) { step in
|
|
1394
|
+
switch step {
|
|
1395
|
+
case .step2: Step2View()
|
|
1396
|
+
case .step3: Step3View()
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// RIGHT - Sheets for modals
|
|
1402
|
+
.sheet(isPresented: $showSettings) {
|
|
1403
|
+
SettingsView() // Self-contained modal
|
|
1404
|
+
}
|
|
1405
|
+
```
|
|
1406
|
+
</anti_pattern>
|
|
1407
|
+
|
|
1408
|
+
<anti_pattern name="Not making navigation types Hashable">
|
|
1409
|
+
**Problem:** Forgetting to conform to Hashable for navigationDestination types
|
|
1410
|
+
|
|
1411
|
+
**Why it's bad:**
|
|
1412
|
+
- Compiler error: navigationDestination requires Hashable
|
|
1413
|
+
- NavigationPath can't store non-Hashable types
|
|
1414
|
+
|
|
1415
|
+
**Instead:** Always make route types Hashable (and Codable for persistence)
|
|
1416
|
+
```swift
|
|
1417
|
+
// WRONG
|
|
1418
|
+
struct Route {
|
|
1419
|
+
let id: Int
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// RIGHT
|
|
1423
|
+
struct Route: Hashable {
|
|
1424
|
+
let id: Int
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// EVEN BETTER - also Codable for persistence
|
|
1428
|
+
enum Route: Hashable, Codable {
|
|
1429
|
+
case detail(id: Int)
|
|
1430
|
+
case settings
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
</anti_pattern>
|
|
1434
|
+
|
|
1435
|
+
<anti_pattern name="Creating separate NavigationStack per TabView tab without independent state">
|
|
1436
|
+
**Problem:** Sharing NavigationPath between tabs
|
|
1437
|
+
|
|
1438
|
+
**Why it's bad:**
|
|
1439
|
+
- Tabs should have independent navigation stacks
|
|
1440
|
+
- Switching tabs loses navigation context
|
|
1441
|
+
- Breaks expected tab behavior
|
|
1442
|
+
|
|
1443
|
+
**Instead:** Each tab gets its own NavigationStack and path
|
|
1444
|
+
```swift
|
|
1445
|
+
// WRONG
|
|
1446
|
+
@State private var path = NavigationPath()
|
|
1447
|
+
|
|
1448
|
+
TabView {
|
|
1449
|
+
NavigationStack(path: $path) { HomeView() }
|
|
1450
|
+
.tabItem { Label("Home", systemImage: "house") }
|
|
1451
|
+
|
|
1452
|
+
NavigationStack(path: $path) { SearchView() } // ❌ Shared path
|
|
1453
|
+
.tabItem { Label("Search", systemImage: "magnifyingglass") }
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// RIGHT
|
|
1457
|
+
@State private var homePath = NavigationPath()
|
|
1458
|
+
@State private var searchPath = NavigationPath()
|
|
1459
|
+
|
|
1460
|
+
TabView {
|
|
1461
|
+
NavigationStack(path: $homePath) { HomeView() }
|
|
1462
|
+
.tabItem { Label("Home", systemImage: "house") }
|
|
1463
|
+
|
|
1464
|
+
NavigationStack(path: $searchPath) { SearchView() } // ✅ Independent
|
|
1465
|
+
.tabItem { Label("Search", systemImage: "magnifyingglass") }
|
|
1466
|
+
}
|
|
1467
|
+
```
|
|
1468
|
+
</anti_pattern>
|
|
1469
|
+
</anti_patterns>
|
|
1470
|
+
|
|
1471
|
+
## Sources
|
|
1472
|
+
|
|
1473
|
+
- [Hacking with Swift: Programmatic navigation with NavigationStack](https://www.hackingwithswift.com/books/ios-swiftui/programmatic-navigation-with-navigationstack)
|
|
1474
|
+
- [AzamSharp: Navigation Patterns in SwiftUI](https://azamsharp.com/2024/07/29/navigation-patterns-in-swiftui.html)
|
|
1475
|
+
- [tanaschita: How to use NavigationPath for routing in SwiftUI](https://tanaschita.com/swiftui-navigationpath/)
|
|
1476
|
+
- [Swift with Majid: Mastering NavigationStack in SwiftUI. Navigator Pattern](https://swiftwithmajid.com/2022/06/15/mastering-navigationstack-in-swiftui-navigator-pattern/)
|
|
1477
|
+
- [Medium: Mastering Navigation in SwiftUI: The 2025 Guide](https://medium.com/@dinaga119/mastering-navigation-in-swiftui-the-2025-guide-to-clean-scalable-routing-bbcb6dbce929)
|
|
1478
|
+
- [Swift Anytime: How to use Coordinator Pattern in SwiftUI](https://www.swiftanytime.com/blog/coordinator-pattern-in-swiftui)
|
|
1479
|
+
- [SwiftLee: Deeplink URL handling in SwiftUI](https://www.avanderlee.com/swiftui/deeplink-url-handling/)
|
|
1480
|
+
- [Michael Long: Advanced Deep Linking in SwiftUI](https://michaellong.medium.com/advanced-deep-linking-in-swiftui-c0085be83e7c)
|
|
1481
|
+
- [Swift with Majid: Deep linking for local notifications in SwiftUI](https://swiftwithmajid.com/2024/04/09/deep-linking-for-local-notifications-in-swiftui/)
|
|
1482
|
+
- [Sarunw: Bottom Sheet in SwiftUI on iOS 16 with presentationDetents](https://sarunw.com/posts/swiftui-bottom-sheet/)
|
|
1483
|
+
- [Apple Developer: presentationDetents(_:)](https://developer.apple.com/documentation/swiftui/view/presentationdetents(_:))
|
|
1484
|
+
- [Hacking with Swift: What's new in SwiftUI for iOS 18](https://www.hackingwithswift.com/articles/270/whats-new-in-swiftui-for-ios-18)
|
|
1485
|
+
- [iOS Coffee Break: Using SwiftUI's Improved TabView with Sidebar on iOS 18](https://www.ioscoffeebreak.com/issue/issue34)
|
|
1486
|
+
- [AppCoda: What's New in SwiftUI for iOS 18](https://www.appcoda.com/swiftui-ios-18/)
|
|
1487
|
+
- [Medium: Getting Started with the Improved TabView in iOS 18](https://medium.com/@jpmtech/getting-started-with-the-improved-tabview-in-ios-18-111974b70db9)
|
|
1488
|
+
- [Apple Developer: Enhancing your app's content with tab navigation](https://developer.apple.com/documentation/swiftui/enhancing-your-app-content-with-tab-navigation)
|
|
1489
|
+
- [Apple Developer: NavigationPath](https://developer.apple.com/documentation/swiftui/navigationpath)
|
|
1490
|
+
- [DEV Community: Modern Navigation in SwiftUI](https://dev.to/sebastienlato/modern-navigation-in-swiftui-1c8g)
|
|
1491
|
+
- [Medium: Mastering Navigation in SwiftUI Using Coordinator Pattern](https://medium.com/@dikidwid0/mastering-navigation-in-swiftui-using-coordinator-pattern-833396c67db5)
|
|
1492
|
+
- [QuickBird Studios: How to Use the Coordinator Pattern in SwiftUI](https://quickbirdstudios.com/blog/coordinator-pattern-in-swiftui/)
|