@kata-sh/cli 0.1.0 → 0.1.2

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.
Files changed (199) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +156 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +56 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +95 -0
  9. package/dist/resource-loader.d.ts +18 -0
  10. package/dist/resource-loader.js +50 -0
  11. package/dist/wizard.d.ts +15 -0
  12. package/dist/wizard.js +159 -0
  13. package/package.json +50 -21
  14. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  15. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  16. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  17. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  18. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  19. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  20. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  21. package/pkg/package.json +8 -0
  22. package/scripts/postinstall.js +45 -0
  23. package/src/resources/AGENTS.md +108 -0
  24. package/src/resources/KATA-WORKFLOW.md +661 -0
  25. package/src/resources/agents/researcher.md +29 -0
  26. package/src/resources/agents/scout.md +56 -0
  27. package/src/resources/agents/worker.md +31 -0
  28. package/src/resources/extensions/ask-user-questions.ts +200 -0
  29. package/src/resources/extensions/bg-shell/index.ts +2758 -0
  30. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  31. package/src/resources/extensions/browser-tools/core.js +1057 -0
  32. package/src/resources/extensions/browser-tools/index.ts +4916 -0
  33. package/src/resources/extensions/browser-tools/package.json +20 -0
  34. package/src/resources/extensions/context7/index.ts +428 -0
  35. package/src/resources/extensions/context7/package.json +11 -0
  36. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  37. package/src/resources/extensions/github/formatters.ts +207 -0
  38. package/src/resources/extensions/github/gh-api.ts +537 -0
  39. package/src/resources/extensions/github/index.ts +778 -0
  40. package/src/resources/extensions/kata/activity-log.ts +88 -0
  41. package/src/resources/extensions/kata/auto.ts +2786 -0
  42. package/src/resources/extensions/kata/commands.ts +355 -0
  43. package/src/resources/extensions/kata/crash-recovery.ts +85 -0
  44. package/src/resources/extensions/kata/dashboard-overlay.ts +516 -0
  45. package/src/resources/extensions/kata/docs/preferences-reference.md +103 -0
  46. package/src/resources/extensions/kata/doctor.ts +683 -0
  47. package/src/resources/extensions/kata/files.ts +730 -0
  48. package/src/resources/extensions/kata/gitignore.ts +165 -0
  49. package/src/resources/extensions/kata/guided-flow.ts +976 -0
  50. package/src/resources/extensions/kata/index.ts +556 -0
  51. package/src/resources/extensions/kata/metrics.ts +397 -0
  52. package/src/resources/extensions/kata/observability-validator.ts +408 -0
  53. package/src/resources/extensions/kata/package.json +11 -0
  54. package/src/resources/extensions/kata/paths.ts +346 -0
  55. package/src/resources/extensions/kata/preferences.ts +695 -0
  56. package/src/resources/extensions/kata/prompt-loader.ts +50 -0
  57. package/src/resources/extensions/kata/prompts/complete-milestone.md +25 -0
  58. package/src/resources/extensions/kata/prompts/complete-slice.md +27 -0
  59. package/src/resources/extensions/kata/prompts/discuss.md +151 -0
  60. package/src/resources/extensions/kata/prompts/doctor-heal.md +29 -0
  61. package/src/resources/extensions/kata/prompts/execute-task.md +64 -0
  62. package/src/resources/extensions/kata/prompts/guided-complete-slice.md +1 -0
  63. package/src/resources/extensions/kata/prompts/guided-discuss-milestone.md +3 -0
  64. package/src/resources/extensions/kata/prompts/guided-discuss-slice.md +59 -0
  65. package/src/resources/extensions/kata/prompts/guided-execute-task.md +1 -0
  66. package/src/resources/extensions/kata/prompts/guided-plan-milestone.md +23 -0
  67. package/src/resources/extensions/kata/prompts/guided-plan-slice.md +1 -0
  68. package/src/resources/extensions/kata/prompts/guided-research-slice.md +11 -0
  69. package/src/resources/extensions/kata/prompts/guided-resume-task.md +1 -0
  70. package/src/resources/extensions/kata/prompts/plan-milestone.md +47 -0
  71. package/src/resources/extensions/kata/prompts/plan-slice.md +63 -0
  72. package/src/resources/extensions/kata/prompts/queue.md +85 -0
  73. package/src/resources/extensions/kata/prompts/reassess-roadmap.md +48 -0
  74. package/src/resources/extensions/kata/prompts/replan-slice.md +39 -0
  75. package/src/resources/extensions/kata/prompts/research-milestone.md +37 -0
  76. package/src/resources/extensions/kata/prompts/research-slice.md +28 -0
  77. package/src/resources/extensions/kata/prompts/run-uat.md +109 -0
  78. package/src/resources/extensions/kata/prompts/system.md +341 -0
  79. package/src/resources/extensions/kata/session-forensics.ts +550 -0
  80. package/src/resources/extensions/kata/skill-discovery.ts +137 -0
  81. package/src/resources/extensions/kata/state.ts +509 -0
  82. package/src/resources/extensions/kata/templates/context.md +76 -0
  83. package/src/resources/extensions/kata/templates/decisions.md +8 -0
  84. package/src/resources/extensions/kata/templates/milestone-summary.md +73 -0
  85. package/src/resources/extensions/kata/templates/plan.md +133 -0
  86. package/src/resources/extensions/kata/templates/preferences.md +15 -0
  87. package/src/resources/extensions/kata/templates/project.md +31 -0
  88. package/src/resources/extensions/kata/templates/reassessment.md +28 -0
  89. package/src/resources/extensions/kata/templates/requirements.md +81 -0
  90. package/src/resources/extensions/kata/templates/research.md +46 -0
  91. package/src/resources/extensions/kata/templates/roadmap.md +118 -0
  92. package/src/resources/extensions/kata/templates/slice-context.md +58 -0
  93. package/src/resources/extensions/kata/templates/slice-summary.md +99 -0
  94. package/src/resources/extensions/kata/templates/state.md +19 -0
  95. package/src/resources/extensions/kata/templates/task-plan.md +52 -0
  96. package/src/resources/extensions/kata/templates/task-summary.md +57 -0
  97. package/src/resources/extensions/kata/templates/uat.md +54 -0
  98. package/src/resources/extensions/kata/tests/activity-log-prune.test.ts +327 -0
  99. package/src/resources/extensions/kata/tests/auto-preflight.test.ts +97 -0
  100. package/src/resources/extensions/kata/tests/auto-supervisor.test.mjs +53 -0
  101. package/src/resources/extensions/kata/tests/complete-milestone.test.ts +317 -0
  102. package/src/resources/extensions/kata/tests/cost-projection.test.ts +160 -0
  103. package/src/resources/extensions/kata/tests/derive-state-deps.test.ts +477 -0
  104. package/src/resources/extensions/kata/tests/derive-state.test.ts +1013 -0
  105. package/src/resources/extensions/kata/tests/doctor.test.ts +718 -0
  106. package/src/resources/extensions/kata/tests/idle-recovery.test.ts +490 -0
  107. package/src/resources/extensions/kata/tests/metrics-io.test.ts +254 -0
  108. package/src/resources/extensions/kata/tests/metrics.test.ts +217 -0
  109. package/src/resources/extensions/kata/tests/must-have-parser.test.ts +309 -0
  110. package/src/resources/extensions/kata/tests/parsers.test.ts +1257 -0
  111. package/src/resources/extensions/kata/tests/plan-milestone.test.ts +185 -0
  112. package/src/resources/extensions/kata/tests/plan-quality-validator.test.ts +386 -0
  113. package/src/resources/extensions/kata/tests/reassess-prompt.test.ts +208 -0
  114. package/src/resources/extensions/kata/tests/replan-slice.test.ts +686 -0
  115. package/src/resources/extensions/kata/tests/requirements.test.ts +151 -0
  116. package/src/resources/extensions/kata/tests/resolve-ts-hooks.mjs +17 -0
  117. package/src/resources/extensions/kata/tests/resolve-ts.mjs +11 -0
  118. package/src/resources/extensions/kata/tests/run-uat.test.ts +383 -0
  119. package/src/resources/extensions/kata/tests/unit-runtime.test.ts +388 -0
  120. package/src/resources/extensions/kata/tests/workspace-index.test.ts +118 -0
  121. package/src/resources/extensions/kata/tests/worktree.test.ts +222 -0
  122. package/src/resources/extensions/kata/types.ts +159 -0
  123. package/src/resources/extensions/kata/unit-runtime.ts +163 -0
  124. package/src/resources/extensions/kata/workspace-index.ts +203 -0
  125. package/src/resources/extensions/kata/worktree.ts +182 -0
  126. package/src/resources/extensions/mac-tools/index.ts +852 -0
  127. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  128. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  129. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  130. package/src/resources/extensions/search-the-web/format.ts +258 -0
  131. package/src/resources/extensions/search-the-web/http.ts +238 -0
  132. package/src/resources/extensions/search-the-web/index.ts +68 -0
  133. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  134. package/src/resources/extensions/search-the-web/tool-llm-context.ts +404 -0
  135. package/src/resources/extensions/search-the-web/tool-search.ts +503 -0
  136. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  137. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  138. package/src/resources/extensions/shared/interview-ui.ts +822 -0
  139. package/src/resources/extensions/shared/next-action-ui.ts +235 -0
  140. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  141. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  142. package/src/resources/extensions/shared/ui.ts +400 -0
  143. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  144. package/src/resources/extensions/slash-commands/audit.ts +92 -0
  145. package/src/resources/extensions/slash-commands/create-extension.ts +375 -0
  146. package/src/resources/extensions/slash-commands/create-slash-command.ts +280 -0
  147. package/src/resources/extensions/slash-commands/index.ts +12 -0
  148. package/src/resources/extensions/slash-commands/kata-run.ts +34 -0
  149. package/src/resources/extensions/subagent/agents.ts +126 -0
  150. package/src/resources/extensions/subagent/index.ts +1293 -0
  151. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  152. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  153. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  154. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  155. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  156. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  157. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  158. package/src/resources/skills/swiftui/SKILL.md +208 -0
  159. package/src/resources/skills/swiftui/references/animations.md +921 -0
  160. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  161. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  162. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  163. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  164. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  165. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  166. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  167. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  168. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  169. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  170. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  171. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  172. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  173. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  174. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  175. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
  176. package/dist/commands/task.d.ts +0 -9
  177. package/dist/commands/task.d.ts.map +0 -1
  178. package/dist/commands/task.js +0 -129
  179. package/dist/commands/task.js.map +0 -1
  180. package/dist/commands/task.test.d.ts +0 -2
  181. package/dist/commands/task.test.d.ts.map +0 -1
  182. package/dist/commands/task.test.js +0 -169
  183. package/dist/commands/task.test.js.map +0 -1
  184. package/dist/e2e/task-e2e.test.d.ts +0 -2
  185. package/dist/e2e/task-e2e.test.d.ts.map +0 -1
  186. package/dist/e2e/task-e2e.test.js +0 -173
  187. package/dist/e2e/task-e2e.test.js.map +0 -1
  188. package/dist/index.d.ts +0 -3
  189. package/dist/index.d.ts.map +0 -1
  190. package/dist/index.js +0 -93
  191. package/dist/index.js.map +0 -1
  192. package/dist/slug.d.ts +0 -2
  193. package/dist/slug.d.ts.map +0 -1
  194. package/dist/slug.js +0 -12
  195. package/dist/slug.js.map +0 -1
  196. package/dist/slug.test.d.ts +0 -2
  197. package/dist/slug.test.d.ts.map +0 -1
  198. package/dist/slug.test.js +0 -32
  199. package/dist/slug.test.js.map +0 -1
@@ -0,0 +1,1443 @@
1
+ <overview>
2
+ SwiftUI state management is fundamentally different from imperative UI frameworks. You describe what the UI should look like for any given state, and SwiftUI handles the updates when state changes.
3
+
4
+ **Read this file when:** Building views that need to respond to data changes, sharing data between views, choosing property wrappers, debugging state issues, or migrating from ObservableObject patterns.
5
+
6
+ **Key insight:** SwiftUI uses a declarative, unidirectional data flow. State flows down through the view hierarchy via properties. Changes flow up through bindings or actions. You describe state, not mutations.
7
+
8
+ **Modern SwiftUI (iOS 17+)** uses the @Observable macro for reference types, eliminating most needs for ObservableObject, @Published, @StateObject, @ObservedObject, and @EnvironmentObject. The mental model is simpler: value types use @State/@Binding, reference types use @Observable with @State/@Bindable.
9
+ </overview>
10
+
11
+ <property_wrappers>
12
+ ## Property Wrappers
13
+
14
+ <wrapper name="@State">
15
+ **Purpose:** Manage mutable value types owned by a single view. The source of truth for simple, view-local data.
16
+
17
+ **When to use:** Simple values (Int, String, Bool, Array, struct) that belong to this view and need to trigger UI updates when changed.
18
+
19
+ **Ownership:** The view owns this data. SwiftUI manages its lifecycle.
20
+
21
+ **Lifecycle:** Persists across view body recomputes. Reset when the view is removed from the hierarchy and recreated with a new identity.
22
+
23
+ ```swift
24
+ struct CounterView: View {
25
+ @State private var count = 0
26
+ @State private var isExpanded = false
27
+
28
+ var body: some View {
29
+ VStack {
30
+ Text("Count: \(count)")
31
+
32
+ Button("Increment") {
33
+ count += 1
34
+ }
35
+
36
+ if isExpanded {
37
+ Text("Details about count...")
38
+ }
39
+
40
+ Toggle("Show Details", isOn: $isExpanded)
41
+ }
42
+ }
43
+ }
44
+ ```
45
+
46
+ **With @Observable classes (iOS 17+):**
47
+ ```swift
48
+ @Observable
49
+ class ViewModel {
50
+ var items: [String] = []
51
+ var selectedItem: String?
52
+ }
53
+
54
+ struct ContentView: View {
55
+ @State private var viewModel = ViewModel()
56
+
57
+ var body: some View {
58
+ List(viewModel.items, id: \.self) { item in
59
+ Text(item)
60
+ }
61
+ }
62
+ }
63
+ ```
64
+
65
+ **Common mistakes:**
66
+ - Making @State public (should be private to enforce view-local ownership)
67
+ - Using @State for data passed from a parent (use @Binding or receive as plain property)
68
+ - Not initializing @State with a value
69
+ - Using @State with ObservableObject classes pre-iOS 17 (use @StateObject instead)
70
+ </wrapper>
71
+
72
+ <wrapper name="@Binding">
73
+ **Purpose:** Create a two-way connection to state owned by another view. Allows a child view to read and write a parent's state without owning it.
74
+
75
+ **When to use:** Passing writable access to value type data from parent to child. The child needs to modify data it doesn't own.
76
+
77
+ **Ownership:** The parent owns the data. This view has read-write access via reference.
78
+
79
+ **Lifecycle:** Tied to the source of truth it references.
80
+
81
+ ```swift
82
+ struct ParentView: View {
83
+ @State private var username = ""
84
+
85
+ var body: some View {
86
+ VStack {
87
+ Text("Hello, \(username)")
88
+ UsernameField(username: $username)
89
+ }
90
+ }
91
+ }
92
+
93
+ struct UsernameField: View {
94
+ @Binding var username: String
95
+
96
+ var body: some View {
97
+ TextField("Enter name", text: $username)
98
+ .textFieldStyle(.roundedBorder)
99
+ .padding()
100
+ }
101
+ }
102
+ ```
103
+
104
+ **With custom controls:**
105
+ ```swift
106
+ struct ToggleButton: View {
107
+ @Binding var isOn: Bool
108
+ let label: String
109
+
110
+ var body: some View {
111
+ Button(label) {
112
+ isOn.toggle()
113
+ }
114
+ .foregroundStyle(isOn ? .green : .gray)
115
+ }
116
+ }
117
+
118
+ // Usage
119
+ struct ContentView: View {
120
+ @State private var notificationsEnabled = false
121
+
122
+ var body: some View {
123
+ ToggleButton(
124
+ isOn: $notificationsEnabled,
125
+ label: "Notifications"
126
+ )
127
+ }
128
+ }
129
+ ```
130
+
131
+ **Common mistakes:**
132
+ - Providing a default value to @Binding (bindings are always passed from outside)
133
+ - Making @Binding private (it must be accessible to receive the binding)
134
+ - Passing the value without $ prefix (passes a copy, not a binding)
135
+ - Using @Binding when the child shouldn't modify the value (use a plain property instead)
136
+ </wrapper>
137
+
138
+ <wrapper name="@Observable">
139
+ **Purpose:** Mark a class as observable so SwiftUI automatically tracks property changes and updates views. Replaces ObservableObject protocol in iOS 17+.
140
+
141
+ **When to use:** Reference type data models shared across multiple views. Complex state that benefits from reference semantics. Data that needs to be passed down the view hierarchy.
142
+
143
+ **Ownership:** Created and owned by a view using @State, or passed through @Environment for app-wide access.
144
+
145
+ **Lifecycle:** Follows standard Swift reference type lifecycle. When stored in @State, survives view body recomputes.
146
+
147
+ ```swift
148
+ import Observation
149
+
150
+ @Observable
151
+ class ShoppingCart {
152
+ var items: [Item] = []
153
+ var discount: Double = 0.0
154
+
155
+ var total: Double {
156
+ let subtotal = items.reduce(0) { $0 + $1.price }
157
+ return subtotal * (1 - discount)
158
+ }
159
+
160
+ func addItem(_ item: Item) {
161
+ items.append(item)
162
+ }
163
+ }
164
+
165
+ struct StoreView: View {
166
+ @State private var cart = ShoppingCart()
167
+
168
+ var body: some View {
169
+ VStack {
170
+ CartSummary(cart: cart)
171
+ ProductList(cart: cart)
172
+ }
173
+ }
174
+ }
175
+
176
+ struct CartSummary: View {
177
+ var cart: ShoppingCart // Plain property, no wrapper needed
178
+
179
+ var body: some View {
180
+ Text("Total: $\(cart.total, specifier: "%.2f")")
181
+ .font(.headline)
182
+ }
183
+ }
184
+ ```
185
+
186
+ **With @ObservationIgnored for non-tracked properties:**
187
+ ```swift
188
+ @Observable
189
+ class UserSession {
190
+ var username: String = ""
191
+ var loginCount: Int = 0
192
+
193
+ @ObservationIgnored
194
+ var temporaryCache: [String: Any] = [:] // Won't trigger view updates
195
+ }
196
+ ```
197
+
198
+ **Common mistakes:**
199
+ - Using @Published with @Observable (not needed, all properties are observed by default)
200
+ - Forgetting to import Observation
201
+ - Using @StateObject instead of @State for @Observable classes
202
+ - Not using @ObservationIgnored for properties that shouldn't trigger updates (like caches, formatters)
203
+ </wrapper>
204
+
205
+ <wrapper name="@Bindable">
206
+ **Purpose:** Create bindings to properties of @Observable objects when the view doesn't own the object. Bridges @Observable with SwiftUI's $ binding syntax.
207
+
208
+ **When to use:** You have an @Observable object passed from a parent, and you need to create two-way bindings to its properties (for TextField, Toggle, etc.).
209
+
210
+ **Ownership:** The view doesn't own the object. It's passed from outside.
211
+
212
+ **Lifecycle:** Tied to the lifecycle of the @Observable object it references.
213
+
214
+ ```swift
215
+ @Observable
216
+ class FormData {
217
+ var name: String = ""
218
+ var email: String = ""
219
+ var agreedToTerms: Bool = false
220
+ }
221
+
222
+ struct ParentView: View {
223
+ @State private var formData = FormData()
224
+
225
+ var body: some View {
226
+ FormView(formData: formData)
227
+ }
228
+ }
229
+
230
+ struct FormView: View {
231
+ @Bindable var formData: FormData
232
+
233
+ var body: some View {
234
+ Form {
235
+ TextField("Name", text: $formData.name)
236
+ TextField("Email", text: $formData.email)
237
+ Toggle("I agree to terms", isOn: $formData.agreedToTerms)
238
+ }
239
+ }
240
+ }
241
+ ```
242
+
243
+ **Nested child views:**
244
+ ```swift
245
+ struct NestedChildView: View {
246
+ @Bindable var formData: FormData
247
+
248
+ var body: some View {
249
+ // Can still create bindings to properties
250
+ Toggle("Marketing emails", isOn: $formData.agreedToTerms)
251
+ }
252
+ }
253
+ ```
254
+
255
+ **Common mistakes:**
256
+ - Using @Bindable when you own the object (use @State instead)
257
+ - Using @Binding for @Observable objects (use @Bindable for reference types)
258
+ - Forgetting that @Bindable doesn't work with ObservableObject (legacy pattern)
259
+ - Using @ObservedObject instead of @Bindable for iOS 17+ code
260
+ </wrapper>
261
+
262
+ <wrapper name="@Environment">
263
+ **Purpose:** Read values from SwiftUI's environment or inject custom values accessible throughout the view hierarchy. Replaces @EnvironmentObject in iOS 17+.
264
+
265
+ **When to use:**
266
+ - Accessing system values (colorScheme, locale, dismiss, etc.)
267
+ - Sharing app-wide or subtree-wide state without prop drilling
268
+ - Dependency injection for services and models
269
+
270
+ **Ownership:** Provided by ancestor views or the system. Current view reads it.
271
+
272
+ **Lifecycle:** Managed by the provider. Available to all descendant views.
273
+
274
+ ```swift
275
+ // System environment values
276
+ struct ThemedView: View {
277
+ @Environment(\.colorScheme) var colorScheme
278
+ @Environment(\.dismiss) var dismiss
279
+
280
+ var body: some View {
281
+ VStack {
282
+ Text("Current theme: \(colorScheme == .dark ? "Dark" : "Light")")
283
+
284
+ Button("Close") {
285
+ dismiss()
286
+ }
287
+ }
288
+ .foregroundStyle(colorScheme == .dark ? .white : .black)
289
+ }
290
+ }
291
+ ```
292
+
293
+ **Custom environment values (iOS 17+):**
294
+ ```swift
295
+ @Observable
296
+ class AppSettings {
297
+ var fontSize: Double = 16
298
+ var accentColor: Color = .blue
299
+ }
300
+
301
+ // In your app root
302
+ @main
303
+ struct MyApp: App {
304
+ @State private var settings = AppSettings()
305
+
306
+ var body: some Scene {
307
+ WindowGroup {
308
+ ContentView()
309
+ .environment(settings)
310
+ }
311
+ }
312
+ }
313
+
314
+ // Access in any descendant view
315
+ struct SettingsView: View {
316
+ @Environment(AppSettings.self) var settings
317
+
318
+ var body: some View {
319
+ VStack {
320
+ Text("Font size: \(settings.fontSize)")
321
+ ColorPicker("Accent", selection: $settings.accentColor)
322
+ }
323
+ }
324
+ }
325
+ ```
326
+
327
+ **Legacy custom environment values (pre-iOS 17):**
328
+ ```swift
329
+ private struct ThemeKey: EnvironmentKey {
330
+ static let defaultValue = Theme.light
331
+ }
332
+
333
+ extension EnvironmentValues {
334
+ var theme: Theme {
335
+ get { self[ThemeKey.self] }
336
+ set { self[ThemeKey.self] = newValue }
337
+ }
338
+ }
339
+
340
+ // Usage
341
+ struct ContentView: View {
342
+ @Environment(\.theme) var theme
343
+
344
+ var body: some View {
345
+ Text("Hello")
346
+ .foregroundStyle(theme.textColor)
347
+ }
348
+ }
349
+ ```
350
+
351
+ **Common mistakes:**
352
+ - Using @EnvironmentObject instead of @Environment for iOS 17+ code
353
+ - Not providing the environment value before accessing it (runtime crash)
354
+ - Overusing environment for data that should be passed as properties
355
+ - Using environment for frequently changing values (can cause unnecessary updates)
356
+ </wrapper>
357
+
358
+ <wrapper name="@AppStorage">
359
+ **Purpose:** Read and write UserDefaults values with automatic UI updates when the value changes.
360
+
361
+ **When to use:** Storing user preferences, settings, or small amounts of persistent data that should survive app relaunches.
362
+
363
+ **Ownership:** Backed by UserDefaults. View has read-write access.
364
+
365
+ **Lifecycle:** Persists between app launches until explicitly removed.
366
+
367
+ ```swift
368
+ struct SettingsView: View {
369
+ @AppStorage("username") private var username = "Guest"
370
+ @AppStorage("notificationsEnabled") private var notificationsEnabled = true
371
+ @AppStorage("theme") private var theme = "system"
372
+
373
+ var body: some View {
374
+ Form {
375
+ TextField("Username", text: $username)
376
+
377
+ Toggle("Notifications", isOn: $notificationsEnabled)
378
+
379
+ Picker("Theme", selection: $theme) {
380
+ Text("System").tag("system")
381
+ Text("Light").tag("light")
382
+ Text("Dark").tag("dark")
383
+ }
384
+ }
385
+ }
386
+ }
387
+ ```
388
+
389
+ **With custom UserDefaults suite:**
390
+ ```swift
391
+ struct SharedSettingsView: View {
392
+ @AppStorage("syncEnabled", store: UserDefaults(suiteName: "group.com.example.app"))
393
+ private var syncEnabled = false
394
+
395
+ var body: some View {
396
+ Toggle("Sync", isOn: $syncEnabled)
397
+ }
398
+ }
399
+ ```
400
+
401
+ **Supported types:** Bool, Int, Double, String, URL, Data
402
+
403
+ **Common mistakes:**
404
+ - Storing sensitive data (UserDefaults is not encrypted)
405
+ - Storing large amounts of data (performance degradation)
406
+ - Using for data that changes frequently during a session (use @State instead)
407
+ - Not providing a default value
408
+ - Assuming cross-app synchronization (requires App Groups configuration)
409
+ </wrapper>
410
+
411
+ <wrapper name="@SceneStorage">
412
+ **Purpose:** Automatic state restoration per scene. Saves and restores values when the app is backgrounded/foregrounded or scenes are destroyed/recreated.
413
+
414
+ **When to use:** Preserving UI state for state restoration (selected tab, scroll position, current navigation path, form data).
415
+
416
+ **Ownership:** Managed per scene by the system. View has read-write access.
417
+
418
+ **Lifecycle:** Persists when app backgrounds. Destroyed when user explicitly kills the app from the app switcher.
419
+
420
+ ```swift
421
+ struct ContentView: View {
422
+ @SceneStorage("selectedTab") private var selectedTab = "home"
423
+
424
+ var body: some View {
425
+ TabView(selection: $selectedTab) {
426
+ HomeView()
427
+ .tabItem { Label("Home", systemImage: "house") }
428
+ .tag("home")
429
+
430
+ ProfileView()
431
+ .tabItem { Label("Profile", systemImage: "person") }
432
+ .tag("profile")
433
+ }
434
+ }
435
+ }
436
+ ```
437
+
438
+ **With navigation state:**
439
+ ```swift
440
+ struct NavigationExample: View {
441
+ @SceneStorage("navigationPath") private var navigationPathData: Data?
442
+ @State private var path = NavigationPath()
443
+
444
+ var body: some View {
445
+ NavigationStack(path: $path) {
446
+ List {
447
+ NavigationLink("Details", value: "details")
448
+ }
449
+ .navigationDestination(for: String.self) { value in
450
+ Text("Showing: \(value)")
451
+ }
452
+ }
453
+ .onAppear {
454
+ if let data = navigationPathData,
455
+ let restored = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
456
+ path = NavigationPath(restored)
457
+ }
458
+ }
459
+ .onChange(of: path) { _, newPath in
460
+ if let representation = newPath.codable,
461
+ let data = try? JSONEncoder().encode(representation) {
462
+ navigationPathData = data
463
+ }
464
+ }
465
+ }
466
+ }
467
+ ```
468
+
469
+ **Supported types:** Bool, Int, Double, String, URL, Data
470
+
471
+ **Common mistakes:**
472
+ - Storing sensitive data (not secure)
473
+ - Storing large amounts of data (Apple warns against this)
474
+ - Expecting data to persist after force-quit (it's cleared)
475
+ - Using for cross-scene data (each scene has its own storage)
476
+ - Not providing a default value
477
+ </wrapper>
478
+
479
+ <wrapper name="@StateObject">
480
+ **Purpose:** Create and own an ObservableObject in a view. Ensures the object survives view body recomputes.
481
+
482
+ **When to use:** Legacy code (pre-iOS 17) when you need to create and own an ObservableObject. For iOS 17+, use @State with @Observable instead.
483
+
484
+ **Ownership:** The view owns and manages the object's lifecycle.
485
+
486
+ **Lifecycle:** Created once when the view is initialized. Survives view body recomputes. Destroyed when view is removed.
487
+
488
+ ```swift
489
+ // Legacy pattern (pre-iOS 17)
490
+ class LegacyViewModel: ObservableObject {
491
+ @Published var count = 0
492
+ @Published var items: [String] = []
493
+ }
494
+
495
+ struct LegacyView: View {
496
+ @StateObject private var viewModel = LegacyViewModel()
497
+
498
+ var body: some View {
499
+ VStack {
500
+ Text("Count: \(viewModel.count)")
501
+
502
+ Button("Increment") {
503
+ viewModel.count += 1
504
+ }
505
+ }
506
+ }
507
+ }
508
+ ```
509
+
510
+ **Common mistakes:**
511
+ - Using @StateObject for iOS 17+ (use @State with @Observable instead)
512
+ - Using @ObservedObject when the view creates the object (causes recreation bugs)
513
+ - Creating @StateObject in non-root views unnecessarily (consider passing from parent)
514
+ - Using for value types (use @State instead)
515
+ </wrapper>
516
+
517
+ <wrapper name="@ObservedObject">
518
+ **Purpose:** Observe an ObservableObject owned by another view. Doesn't create or own the object.
519
+
520
+ **When to use:** Legacy code (pre-iOS 17) when receiving an ObservableObject from a parent. For iOS 17+, pass @Observable objects as plain properties.
521
+
522
+ **Ownership:** Parent or external source owns the object. This view observes it.
523
+
524
+ **Lifecycle:** Tied to the source that owns it.
525
+
526
+ ```swift
527
+ // Legacy pattern (pre-iOS 17)
528
+ class SharedViewModel: ObservableObject {
529
+ @Published var data: String = ""
530
+ }
531
+
532
+ struct ParentView: View {
533
+ @StateObject private var viewModel = SharedViewModel()
534
+
535
+ var body: some View {
536
+ ChildView(viewModel: viewModel)
537
+ }
538
+ }
539
+
540
+ struct ChildView: View {
541
+ @ObservedObject var viewModel: SharedViewModel
542
+
543
+ var body: some View {
544
+ Text(viewModel.data)
545
+ }
546
+ }
547
+ ```
548
+
549
+ **Common mistakes:**
550
+ - Creating the object within the view using @ObservedObject (use @StateObject instead, or @State for @Observable)
551
+ - Using for iOS 17+ code (pass @Observable objects as plain properties)
552
+ - Confusing ownership (if you create it, you own it - use @StateObject not @ObservedObject)
553
+ </wrapper>
554
+ </property_wrappers>
555
+
556
+ <decision_tree>
557
+ ## Choosing the Right Property Wrapper
558
+
559
+ **iOS 17+ Decision Process:**
560
+
561
+ 1. **Is this a value type (Int, String, Bool, struct)?**
562
+ - Owned by this view? → `@State`
563
+ - Passed from parent, needs modification? → `@Binding`
564
+ - Just reading it? → Plain property
565
+
566
+ 2. **Is this an @Observable class?**
567
+ - Created and owned by this view? → `@State`
568
+ - Passed from parent, need to create bindings to properties? → `@Bindable`
569
+ - Passed from parent, just reading? → Plain property
570
+ - App-wide or subtree-wide access? → `@Environment`
571
+
572
+ 3. **Is this a system value or custom environment value?**
573
+ → `@Environment`
574
+
575
+ 4. **Does this need to persist to UserDefaults?**
576
+ → `@AppStorage`
577
+
578
+ 5. **Does this need automatic state restoration per scene?**
579
+ → `@SceneStorage`
580
+
581
+ **Pre-iOS 17 Decision Process:**
582
+
583
+ 1. **Is this a value type?**
584
+ - Owned by this view? → `@State`
585
+ - Passed from parent? → `@Binding`
586
+
587
+ 2. **Is this an ObservableObject?**
588
+ - Created by this view? → `@StateObject`
589
+ - Passed from parent? → `@ObservedObject`
590
+ - App-wide access? → `@EnvironmentObject`
591
+
592
+ 3. **Is this a system value?**
593
+ → `@Environment`
594
+
595
+ **Quick Reference Table:**
596
+
597
+ | Data Type | Ownership | iOS 17+ | Pre-iOS 17 |
598
+ |-----------|-----------|---------|------------|
599
+ | Value type | Own | @State | @State |
600
+ | Value type | Parent owns, need write | @Binding | @Binding |
601
+ | Value type | Parent owns, read only | Plain property | Plain property |
602
+ | @Observable class | Own | @State | N/A |
603
+ | @Observable class | Parent owns, need bindings | @Bindable | N/A |
604
+ | @Observable class | Parent owns, read only | Plain property | N/A |
605
+ | @Observable class | App-wide | @Environment | N/A |
606
+ | ObservableObject | Own | N/A | @StateObject |
607
+ | ObservableObject | Parent owns | N/A | @ObservedObject |
608
+ | ObservableObject | App-wide | N/A | @EnvironmentObject |
609
+ | System values | N/A | @Environment | @Environment |
610
+ | UserDefaults | N/A | @AppStorage | @AppStorage |
611
+ | State restoration | N/A | @SceneStorage | @SceneStorage |
612
+ </decision_tree>
613
+
614
+ <patterns>
615
+ ## Common Patterns
616
+
617
+ <pattern name="Unidirectional Data Flow">
618
+ **Use when:** Building any SwiftUI view hierarchy. This is the fundamental pattern.
619
+
620
+ **Concept:** Data flows down the view hierarchy as properties. Changes flow up through bindings or callbacks. State has a single source of truth.
621
+
622
+ **Implementation:**
623
+ ```swift
624
+ @Observable
625
+ class AppState {
626
+ var items: [Item] = []
627
+ var selectedItemId: UUID?
628
+
629
+ func selectItem(_ id: UUID) {
630
+ selectedItemId = id
631
+ }
632
+
633
+ func addItem(_ item: Item) {
634
+ items.append(item)
635
+ }
636
+ }
637
+
638
+ struct AppView: View {
639
+ @State private var appState = AppState()
640
+
641
+ var body: some View {
642
+ NavigationStack {
643
+ ItemList(
644
+ items: appState.items,
645
+ selectedId: appState.selectedItemId,
646
+ onSelect: { appState.selectItem($0) }
647
+ )
648
+ }
649
+ }
650
+ }
651
+
652
+ struct ItemList: View {
653
+ let items: [Item]
654
+ let selectedId: UUID?
655
+ let onSelect: (UUID) -> Void
656
+
657
+ var body: some View {
658
+ List(items) { item in
659
+ ItemRow(
660
+ item: item,
661
+ isSelected: item.id == selectedId,
662
+ onTap: { onSelect(item.id) }
663
+ )
664
+ }
665
+ }
666
+ }
667
+
668
+ struct ItemRow: View {
669
+ let item: Item
670
+ let isSelected: Bool
671
+ let onTap: () -> Void
672
+
673
+ var body: some View {
674
+ HStack {
675
+ Text(item.name)
676
+ if isSelected {
677
+ Image(systemName: "checkmark")
678
+ }
679
+ }
680
+ .onTapGesture(perform: onTap)
681
+ }
682
+ }
683
+ ```
684
+
685
+ **Considerations:**
686
+ - Clear data flow is easier to debug than bidirectional mutations
687
+ - Callbacks can become verbose for deeply nested hierarchies (consider @Environment)
688
+ - Single source of truth prevents sync issues
689
+ </pattern>
690
+
691
+ <pattern name="Environment Injection">
692
+ **Use when:** Multiple views need access to shared state without prop drilling. Dependency injection for services.
693
+
694
+ **Implementation:**
695
+ ```swift
696
+ @Observable
697
+ class UserSession {
698
+ var isLoggedIn = false
699
+ var username: String?
700
+
701
+ func login(username: String) {
702
+ self.username = username
703
+ isLoggedIn = true
704
+ }
705
+
706
+ func logout() {
707
+ username = nil
708
+ isLoggedIn = false
709
+ }
710
+ }
711
+
712
+ @main
713
+ struct MyApp: App {
714
+ @State private var session = UserSession()
715
+
716
+ var body: some Scene {
717
+ WindowGroup {
718
+ ContentView()
719
+ .environment(session)
720
+ }
721
+ }
722
+ }
723
+
724
+ struct ContentView: View {
725
+ @Environment(UserSession.self) private var session
726
+
727
+ var body: some View {
728
+ if session.isLoggedIn {
729
+ HomeView()
730
+ } else {
731
+ LoginView()
732
+ }
733
+ }
734
+ }
735
+
736
+ struct LoginView: View {
737
+ @Environment(UserSession.self) private var session
738
+ @State private var username = ""
739
+
740
+ var body: some View {
741
+ VStack {
742
+ TextField("Username", text: $username)
743
+ Button("Login") {
744
+ session.login(username: username)
745
+ }
746
+ }
747
+ }
748
+ }
749
+
750
+ struct HomeView: View {
751
+ @Environment(UserSession.self) private var session
752
+
753
+ var body: some View {
754
+ VStack {
755
+ Text("Welcome, \(session.username ?? "")")
756
+ Button("Logout") {
757
+ session.logout()
758
+ }
759
+ }
760
+ }
761
+ }
762
+ ```
763
+
764
+ **Considerations:**
765
+ - Convenient for app-wide state (settings, auth, theme)
766
+ - Runtime crash if environment value not provided
767
+ - Can make testing harder (need to provide environment in previews/tests)
768
+ - Overuse can hide dependencies and make data flow unclear
769
+ </pattern>
770
+
771
+ <pattern name="Derived State">
772
+ **Use when:** Computing values from other state. Avoid storing redundant state.
773
+
774
+ **Implementation:**
775
+ ```swift
776
+ @Observable
777
+ class ShoppingCart {
778
+ var items: [CartItem] = []
779
+ var discountCode: String?
780
+
781
+ // Derived - computed from items
782
+ var subtotal: Double {
783
+ items.reduce(0) { $0 + ($1.price * Double($1.quantity)) }
784
+ }
785
+
786
+ // Derived - computed from subtotal and discountCode
787
+ var discount: Double {
788
+ guard let code = discountCode else { return 0 }
789
+ switch code {
790
+ case "SAVE10": return subtotal * 0.1
791
+ case "SAVE20": return subtotal * 0.2
792
+ default: return 0
793
+ }
794
+ }
795
+
796
+ // Derived - computed from subtotal and discount
797
+ var total: Double {
798
+ subtotal - discount
799
+ }
800
+ }
801
+
802
+ struct CartView: View {
803
+ @State private var cart = ShoppingCart()
804
+
805
+ var body: some View {
806
+ VStack {
807
+ List(cart.items) { item in
808
+ HStack {
809
+ Text(item.name)
810
+ Spacer()
811
+ Text("$\(item.price * Double(item.quantity), specifier: "%.2f")")
812
+ }
813
+ }
814
+
815
+ Divider()
816
+
817
+ HStack {
818
+ Text("Subtotal:")
819
+ Spacer()
820
+ Text("$\(cart.subtotal, specifier: "%.2f")")
821
+ }
822
+
823
+ if cart.discount > 0 {
824
+ HStack {
825
+ Text("Discount:")
826
+ Spacer()
827
+ Text("-$\(cart.discount, specifier: "%.2f")")
828
+ .foregroundStyle(.green)
829
+ }
830
+ }
831
+
832
+ HStack {
833
+ Text("Total:")
834
+ .bold()
835
+ Spacer()
836
+ Text("$\(cart.total, specifier: "%.2f")")
837
+ .bold()
838
+ }
839
+ }
840
+ }
841
+ }
842
+ ```
843
+
844
+ **Considerations:**
845
+ - Computed properties are always in sync with source data
846
+ - No need to manually update derived state
847
+ - Recomputed on every access (cache if expensive)
848
+ - Keep computations simple or consider caching
849
+ </pattern>
850
+
851
+ <pattern name="View-Specific State">
852
+ **Use when:** State only matters for presentation, not business logic (UI-only state like selection, expansion, animation).
853
+
854
+ **Implementation:**
855
+ ```swift
856
+ struct ExpandableCard: View {
857
+ let content: String
858
+ @State private var isExpanded = false // UI state only
859
+
860
+ var body: some View {
861
+ VStack(alignment: .leading) {
862
+ HStack {
863
+ Text(content.prefix(50))
864
+ .lineLimit(isExpanded ? nil : 1)
865
+ Spacer()
866
+ Image(systemName: isExpanded ? "chevron.up" : "chevron.down")
867
+ }
868
+
869
+ if isExpanded {
870
+ Text(content)
871
+ .font(.caption)
872
+ .foregroundStyle(.secondary)
873
+ }
874
+ }
875
+ .padding()
876
+ .background(.quaternary)
877
+ .cornerRadius(8)
878
+ .onTapGesture {
879
+ withAnimation {
880
+ isExpanded.toggle()
881
+ }
882
+ }
883
+ }
884
+ }
885
+ ```
886
+
887
+ **Considerations:**
888
+ - Keeps business logic separate from UI state
889
+ - Resets naturally when view is recreated
890
+ - Makes components self-contained and reusable
891
+ - Consider if state needs to persist (use @SceneStorage for restoration)
892
+ </pattern>
893
+
894
+ <pattern name="Sharing State Between Sibling Views">
895
+ **Use when:** Two sibling views need to share mutable state.
896
+
897
+ **Implementation:**
898
+ ```swift
899
+ struct ParentView: View {
900
+ @State private var searchQuery = "" // Shared state lives in parent
901
+
902
+ var body: some View {
903
+ VStack {
904
+ SearchBar(query: $searchQuery) // Pass binding to both siblings
905
+ SearchResults(query: searchQuery)
906
+ }
907
+ }
908
+ }
909
+
910
+ struct SearchBar: View {
911
+ @Binding var query: String
912
+
913
+ var body: some View {
914
+ TextField("Search", text: $query)
915
+ .textFieldStyle(.roundedBorder)
916
+ .padding()
917
+ }
918
+ }
919
+
920
+ struct SearchResults: View {
921
+ let query: String
922
+
923
+ var body: some View {
924
+ List {
925
+ // Filter results based on query
926
+ Text("Results for: \(query)")
927
+ }
928
+ }
929
+ }
930
+ ```
931
+
932
+ **Considerations:**
933
+ - State lives in lowest common ancestor
934
+ - Clear data flow (parent owns, children use)
935
+ - Siblings can't directly communicate (goes through parent)
936
+ - Consider @Observable model if state becomes complex
937
+ </pattern>
938
+ </patterns>
939
+
940
+ <anti_patterns>
941
+ ## What NOT to Do
942
+
943
+ <anti_pattern name="Creating @ObservedObject in View">
944
+ **Problem:** Creating an ObservableObject with @ObservedObject instead of @StateObject.
945
+
946
+ **Why it's bad:** SwiftUI can recreate views at any time. @ObservedObject doesn't guarantee the object survives, causing data loss, crashes, and unpredictable behavior. The object gets recreated on every view update.
947
+
948
+ **Instead:**
949
+ ```swift
950
+ // WRONG
951
+ struct MyView: View {
952
+ @ObservedObject var viewModel = ViewModel() // ❌ Will be recreated!
953
+ var body: some View { /* ... */ }
954
+ }
955
+
956
+ // RIGHT (pre-iOS 17)
957
+ struct MyView: View {
958
+ @StateObject private var viewModel = ViewModel() // ✅ Survives redraws
959
+ var body: some View { /* ... */ }
960
+ }
961
+
962
+ // RIGHT (iOS 17+)
963
+ @Observable
964
+ class ViewModel {
965
+ var data = ""
966
+ }
967
+
968
+ struct MyView: View {
969
+ @State private var viewModel = ViewModel() // ✅ Modern approach
970
+ var body: some View { /* ... */ }
971
+ }
972
+ ```
973
+ </anti_pattern>
974
+
975
+ <anti_pattern name="Not Making @State Private">
976
+ **Problem:** Declaring @State properties as public or internal.
977
+
978
+ **Why it's bad:** @State is meant for view-local state. Making it public violates encapsulation and suggests the state should be passed from outside (making it not truly @State). Creates confusion about ownership.
979
+
980
+ **Instead:**
981
+ ```swift
982
+ // WRONG
983
+ struct MyView: View {
984
+ @State var count = 0 // ❌ Not private
985
+ var body: some View { /* ... */ }
986
+ }
987
+
988
+ // RIGHT
989
+ struct MyView: View {
990
+ @State private var count = 0 // ✅ Private ownership
991
+ var body: some View { /* ... */ }
992
+ }
993
+
994
+ // If state needs to come from outside:
995
+ struct MyView: View {
996
+ @Binding var count: Int // ✅ Use @Binding instead
997
+ var body: some View { /* ... */ }
998
+ }
999
+ ```
1000
+ </anti_pattern>
1001
+
1002
+ <anti_pattern name="Storing Large Objects in State">
1003
+ **Problem:** Storing large value types or arrays in @State, causing performance issues.
1004
+
1005
+ **Why it's bad:** SwiftUI recreates the view body whenever @State changes. Large value types cause expensive copies. Massive arrays cause performance degradation.
1006
+
1007
+ **Instead:**
1008
+ ```swift
1009
+ // WRONG
1010
+ struct ListView: View {
1011
+ @State private var items: [LargeItem] = loadThousandsOfItems() // ❌ Expensive copies
1012
+ var body: some View { /* ... */ }
1013
+ }
1014
+
1015
+ // RIGHT
1016
+ @Observable
1017
+ class ItemStore {
1018
+ var items: [LargeItem] = [] // Reference type, no copies
1019
+ }
1020
+
1021
+ struct ListView: View {
1022
+ @State private var store = ItemStore() // ✅ Only reference is copied
1023
+ var body: some View { /* ... */ }
1024
+ }
1025
+ ```
1026
+ </anti_pattern>
1027
+
1028
+ <anti_pattern name="Using @Binding Without $">
1029
+ **Problem:** Passing a state value to a child expecting @Binding without the $ prefix.
1030
+
1031
+ **Why it's bad:** Passes a copy of the value instead of a binding. Child's changes don't propagate back to parent.
1032
+
1033
+ **Instead:**
1034
+ ```swift
1035
+ struct ParentView: View {
1036
+ @State private var text = ""
1037
+
1038
+ var body: some View {
1039
+ // WRONG
1040
+ ChildView(text: text) // ❌ Passes copy
1041
+
1042
+ // RIGHT
1043
+ ChildView(text: $text) // ✅ Passes binding
1044
+ }
1045
+ }
1046
+
1047
+ struct ChildView: View {
1048
+ @Binding var text: String
1049
+ var body: some View {
1050
+ TextField("Enter text", text: $text)
1051
+ }
1052
+ }
1053
+ ```
1054
+ </anti_pattern>
1055
+
1056
+ <anti_pattern name="Mutating State in Computed Properties">
1057
+ **Problem:** Changing @State or @Observable properties inside computed properties or body.
1058
+
1059
+ **Why it's bad:** Causes infinite loops or unpredictable update cycles. SwiftUI reads body to determine what to render; mutating state during rendering triggers another render.
1060
+
1061
+ **Instead:**
1062
+ ```swift
1063
+ // WRONG
1064
+ struct MyView: View {
1065
+ @State private var count = 0
1066
+
1067
+ var body: some View {
1068
+ let _ = count += 1 // ❌ Infinite loop!
1069
+ Text("Count: \(count)")
1070
+ }
1071
+ }
1072
+
1073
+ // RIGHT
1074
+ struct MyView: View {
1075
+ @State private var count = 0
1076
+
1077
+ var body: some View {
1078
+ VStack {
1079
+ Text("Count: \(count)")
1080
+ Button("Increment") {
1081
+ count += 1 // ✅ Mutate in response to events
1082
+ }
1083
+ }
1084
+ .onAppear {
1085
+ count = 0 // ✅ Or in lifecycle events
1086
+ }
1087
+ }
1088
+ }
1089
+ ```
1090
+ </anti_pattern>
1091
+
1092
+ <anti_pattern name="Storing Sensitive Data in @AppStorage">
1093
+ **Problem:** Using @AppStorage for passwords, tokens, or other sensitive data.
1094
+
1095
+ **Why it's bad:** UserDefaults is not encrypted. Data is easily accessible to anyone with device access or backup access. Security vulnerability.
1096
+
1097
+ **Instead:**
1098
+ ```swift
1099
+ // WRONG
1100
+ @AppStorage("password") private var password = "" // ❌ Not secure!
1101
+ @AppStorage("authToken") private var token = "" // ❌ Not secure!
1102
+
1103
+ // RIGHT
1104
+ import Security
1105
+
1106
+ class KeychainManager {
1107
+ func save(password: String, for account: String) {
1108
+ // Use Keychain for sensitive data
1109
+ let query: [String: Any] = [
1110
+ kSecClass as String: kSecClassGenericPassword,
1111
+ kSecAttrAccount as String: account,
1112
+ kSecValueData as String: password.data(using: .utf8)!
1113
+ ]
1114
+ SecItemAdd(query as CFDictionary, nil)
1115
+ }
1116
+ }
1117
+
1118
+ // For auth tokens, user credentials, etc.
1119
+ struct SecureView: View {
1120
+ @State private var keychain = KeychainManager()
1121
+
1122
+ var body: some View {
1123
+ Button("Save Password") {
1124
+ keychain.save(password: "secret", for: "user@example.com")
1125
+ }
1126
+ }
1127
+ }
1128
+ ```
1129
+ </anti_pattern>
1130
+
1131
+ <anti_pattern name="Using ObservableObject in iOS 17+ Code">
1132
+ **Problem:** Using ObservableObject, @Published, @StateObject, @ObservedObject, @EnvironmentObject in new iOS 17+ projects.
1133
+
1134
+ **Why it's bad:** The @Observable macro is simpler, more performant, and the recommended approach. Legacy patterns add unnecessary complexity. Better compiler optimization with @Observable.
1135
+
1136
+ **Instead:**
1137
+ ```swift
1138
+ // WRONG (legacy)
1139
+ class ViewModel: ObservableObject {
1140
+ @Published var name = ""
1141
+ @Published var count = 0
1142
+ }
1143
+
1144
+ struct OldView: View {
1145
+ @StateObject private var viewModel = ViewModel()
1146
+ var body: some View { /* ... */ }
1147
+ }
1148
+
1149
+ // RIGHT (iOS 17+)
1150
+ @Observable
1151
+ class ViewModel {
1152
+ var name = ""
1153
+ var count = 0
1154
+ }
1155
+
1156
+ struct ModernView: View {
1157
+ @State private var viewModel = ViewModel()
1158
+ var body: some View { /* ... */ }
1159
+ }
1160
+ ```
1161
+ </anti_pattern>
1162
+
1163
+ <anti_pattern name="Overusing Environment">
1164
+ **Problem:** Putting everything in @Environment, even data that should be passed as properties.
1165
+
1166
+ **Why it's bad:** Hides dependencies, makes views harder to test and preview, unclear data flow, runtime crashes if environment not provided.
1167
+
1168
+ **Instead:**
1169
+ ```swift
1170
+ // WRONG - overusing environment
1171
+ struct ItemRow: View {
1172
+ @Environment(AppState.self) private var appState // ❌ Just to access one property
1173
+
1174
+ var body: some View {
1175
+ Text(appState.currentItem.name)
1176
+ }
1177
+ }
1178
+
1179
+ // RIGHT - explicit dependencies
1180
+ struct ItemRow: View {
1181
+ let item: Item // ✅ Clear dependency
1182
+
1183
+ var body: some View {
1184
+ Text(item.name)
1185
+ }
1186
+ }
1187
+
1188
+ // Environment is good for truly cross-cutting concerns:
1189
+ struct ThemedView: View {
1190
+ @Environment(\.colorScheme) var colorScheme // ✅ System value
1191
+ @Environment(UserSession.self) var session // ✅ App-wide auth state
1192
+
1193
+ var body: some View { /* ... */ }
1194
+ }
1195
+ ```
1196
+ </anti_pattern>
1197
+ </anti_patterns>
1198
+
1199
+ <migration_guide>
1200
+ ## Migrating from Legacy Patterns
1201
+
1202
+ **ObservableObject → @Observable:**
1203
+
1204
+ ```swift
1205
+ // Before (legacy)
1206
+ class ViewModel: ObservableObject {
1207
+ @Published var name: String = ""
1208
+ @Published var count: Int = 0
1209
+ @Published var items: [Item] = []
1210
+
1211
+ private var cache: [String: Any] = [:] // Not published
1212
+ }
1213
+
1214
+ struct OldView: View {
1215
+ @StateObject private var viewModel = ViewModel()
1216
+
1217
+ var body: some View {
1218
+ Text(viewModel.name)
1219
+ }
1220
+ }
1221
+
1222
+ // After (iOS 17+)
1223
+ import Observation
1224
+
1225
+ @Observable
1226
+ class ViewModel {
1227
+ var name: String = ""
1228
+ var count: Int = 0
1229
+ var items: [Item] = []
1230
+
1231
+ @ObservationIgnored
1232
+ private var cache: [String: Any] = [:] // Won't trigger updates
1233
+ }
1234
+
1235
+ struct ModernView: View {
1236
+ @State private var viewModel = ViewModel()
1237
+
1238
+ var body: some View {
1239
+ Text(viewModel.name)
1240
+ }
1241
+ }
1242
+ ```
1243
+
1244
+ **@EnvironmentObject → @Environment:**
1245
+
1246
+ ```swift
1247
+ // Before (legacy)
1248
+ class AppSettings: ObservableObject {
1249
+ @Published var theme: String = "light"
1250
+ }
1251
+
1252
+ @main
1253
+ struct OldApp: App {
1254
+ @StateObject private var settings = AppSettings()
1255
+
1256
+ var body: some Scene {
1257
+ WindowGroup {
1258
+ ContentView()
1259
+ .environmentObject(settings)
1260
+ }
1261
+ }
1262
+ }
1263
+
1264
+ struct OldContentView: View {
1265
+ @EnvironmentObject var settings: AppSettings
1266
+
1267
+ var body: some View {
1268
+ Text("Theme: \(settings.theme)")
1269
+ }
1270
+ }
1271
+
1272
+ // After (iOS 17+)
1273
+ @Observable
1274
+ class AppSettings {
1275
+ var theme: String = "light"
1276
+ }
1277
+
1278
+ @main
1279
+ struct ModernApp: App {
1280
+ @State private var settings = AppSettings()
1281
+
1282
+ var body: some Scene {
1283
+ WindowGroup {
1284
+ ContentView()
1285
+ .environment(settings)
1286
+ }
1287
+ }
1288
+ }
1289
+
1290
+ struct ModernContentView: View {
1291
+ @Environment(AppSettings.self) private var settings
1292
+
1293
+ var body: some View {
1294
+ Text("Theme: \(settings.theme)")
1295
+ }
1296
+ }
1297
+ ```
1298
+
1299
+ **@ObservedObject (child views) → Plain properties:**
1300
+
1301
+ ```swift
1302
+ // Before (legacy)
1303
+ class SharedData: ObservableObject {
1304
+ @Published var value: String = ""
1305
+ }
1306
+
1307
+ struct ParentView: View {
1308
+ @StateObject private var data = SharedData()
1309
+
1310
+ var body: some View {
1311
+ ChildView(data: data)
1312
+ }
1313
+ }
1314
+
1315
+ struct ChildView: View {
1316
+ @ObservedObject var data: SharedData
1317
+
1318
+ var body: some View {
1319
+ Text(data.value)
1320
+ }
1321
+ }
1322
+
1323
+ // After (iOS 17+)
1324
+ @Observable
1325
+ class SharedData {
1326
+ var value: String = ""
1327
+ }
1328
+
1329
+ struct ParentView: View {
1330
+ @State private var data = SharedData()
1331
+
1332
+ var body: some View {
1333
+ ChildView(data: data)
1334
+ }
1335
+ }
1336
+
1337
+ struct ChildView: View {
1338
+ var data: SharedData // Plain property, no wrapper
1339
+
1340
+ var body: some View {
1341
+ Text(data.value)
1342
+ }
1343
+ }
1344
+ ```
1345
+
1346
+ **Creating bindings to @Observable properties:**
1347
+
1348
+ ```swift
1349
+ // Before (legacy)
1350
+ class FormData: ObservableObject {
1351
+ @Published var username: String = ""
1352
+ @Published var email: String = ""
1353
+ }
1354
+
1355
+ struct LegacyForm: View {
1356
+ @ObservedObject var formData: FormData
1357
+
1358
+ var body: some View {
1359
+ Form {
1360
+ TextField("Username", text: $formData.username)
1361
+ TextField("Email", text: $formData.email)
1362
+ }
1363
+ }
1364
+ }
1365
+
1366
+ // After (iOS 17+)
1367
+ @Observable
1368
+ class FormData {
1369
+ var username: String = ""
1370
+ var email: String = ""
1371
+ }
1372
+
1373
+ struct ModernForm: View {
1374
+ @Bindable var formData: FormData
1375
+
1376
+ var body: some View {
1377
+ Form {
1378
+ TextField("Username", text: $formData.username)
1379
+ TextField("Email", text: $formData.email)
1380
+ }
1381
+ }
1382
+ }
1383
+ ```
1384
+
1385
+ **Migration checklist:**
1386
+
1387
+ 1. Add `import Observation` to files using @Observable
1388
+ 2. Replace `class X: ObservableObject` with `@Observable class X`
1389
+ 3. Remove `@Published` from properties (all properties are observed by default)
1390
+ 4. Add `@ObservationIgnored` to properties that shouldn't trigger updates
1391
+ 5. Replace `@StateObject` with `@State` in owning views
1392
+ 6. Replace `@ObservedObject` with plain properties in child views (no wrapper)
1393
+ 7. Replace `@EnvironmentObject` with `@Environment(Type.self)`
1394
+ 8. Replace `.environmentObject(obj)` with `.environment(obj)`
1395
+ 9. Use `@Bindable` when you need to create bindings to @Observable properties
1396
+ 10. Test thoroughly - SwiftUI will warn about missing environment values at runtime
1397
+ </migration_guide>
1398
+
1399
+ <debugging>
1400
+ ## Debugging State Issues
1401
+
1402
+ **State not updating views:**
1403
+ - Verify property is marked with correct wrapper (@State, @Observable)
1404
+ - Check that mutations happen on main thread for UI updates
1405
+ - Ensure @ObservationIgnored isn't on properties that should update views
1406
+ - Confirm view is actually observing the state (proper property wrapper usage)
1407
+
1408
+ **Views updating too much:**
1409
+ - Check if @Observable class is triggering updates from non-UI properties (use @ObservationIgnored)
1410
+ - Verify child views aren't receiving entire model when they only need specific properties
1411
+ - Consider breaking large models into smaller focused models
1412
+ - Use Instruments Time Profiler to identify expensive body computations
1413
+
1414
+ **Runtime crashes:**
1415
+ - "Missing @Environment" - Forgot to provide environment value with `.environment(value)`
1416
+ - Force unwrapping nil @AppStorage or @SceneStorage - Always provide default values
1417
+ - Access to deallocated object - Using @ObservedObject instead of @StateObject for owned objects
1418
+
1419
+ **Previews not working:**
1420
+ - Provide all required @Environment values in preview
1421
+ - Initialize @Binding properties with `.constant(value)` in previews
1422
+ - Ensure @Observable classes are properly initialized
1423
+
1424
+ **Example debugging view:**
1425
+ ```swift
1426
+ struct DebugStateView: View {
1427
+ @State private var viewModel = ViewModel()
1428
+
1429
+ var body: some View {
1430
+ VStack {
1431
+ Text("Count: \(viewModel.count)")
1432
+ Button("Increment") {
1433
+ print("Before: \(viewModel.count)")
1434
+ viewModel.count += 1
1435
+ print("After: \(viewModel.count)")
1436
+ }
1437
+ }
1438
+ // Add debugging modifier
1439
+ ._printChanges() // Prints when view updates and why
1440
+ }
1441
+ }
1442
+ ```
1443
+ </debugging>