@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.
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,1561 @@
1
+ <overview>
2
+ SwiftUI architecture determines how you organize code, manage dependencies, and structure your app for maintainability and scalability. This file covers architectural patterns, project organization, and design decisions.
3
+
4
+ **Read this when:**
5
+ - Starting a new SwiftUI project
6
+ - Deciding between MVVM, TCA, or other patterns
7
+ - Structuring a growing codebase
8
+ - Setting up dependency injection
9
+ - Organizing features into modules
10
+
11
+ **Related files:**
12
+ - state-management.md - State ownership and data flow within architectures
13
+ - navigation.md - Navigation patterns for different architectures
14
+ - networking-async.md - Async operations and where they fit architecturally
15
+ - swiftdata.md - Persistence layer integration with architecture
16
+ </overview>
17
+
18
+ <options>
19
+ ## Available Approaches
20
+
21
+ <option name="SwiftUI Native (Minimal Architecture)">
22
+ **When to use:** Simple apps with limited business logic, prototypes, learning projects, apps with 5-10 screens or fewer
23
+
24
+ **Strengths:**
25
+ - Zero architectural overhead - just build features
26
+ - SwiftUI has MVVM-like patterns built-in (@State acts as ViewModel)
27
+ - Fastest development for small scopes
28
+ - Easy to understand for SwiftUI beginners
29
+ - Works seamlessly with SwiftData (MVVM struggles with SwiftData)
30
+
31
+ **Weaknesses:**
32
+ - Business logic mixes with views as app grows
33
+ - Hard to test (views contain logic)
34
+ - No clear dependency management strategy
35
+ - Doesn't scale well beyond simple apps
36
+ - Can lead to massive view files
37
+
38
+ **Current status:** Apple's default approach, actively recommended for simple apps
39
+
40
+ **Learning curve:** Easy - just use SwiftUI's built-in patterns
41
+
42
+ ```swift
43
+ // Simple counter app with no explicit architecture
44
+ struct ContentView: View {
45
+ @State private var count = 0
46
+ @State private var items: [Item] = []
47
+
48
+ var body: some View {
49
+ VStack {
50
+ Text("Count: \(count)")
51
+ Button("Increment") {
52
+ count += 1
53
+ }
54
+
55
+ List(items) { item in
56
+ Text(item.name)
57
+ }
58
+ }
59
+ .task {
60
+ items = try? await fetchItems()
61
+ }
62
+ }
63
+
64
+ // Business logic embedded in view
65
+ private func fetchItems() async throws -> [Item] {
66
+ let url = URL(string: "https://api.example.com/items")!
67
+ let (data, _) = try await URLSession.shared.data(from: url)
68
+ return try JSONDecoder().decode([Item].self, from: data)
69
+ }
70
+ }
71
+ ```
72
+ </option>
73
+
74
+ <option name="MVVM with @Observable">
75
+ **When to use:** Medium to large apps, apps requiring extensive testing, teams familiar with MVVM, apps with complex business logic
76
+
77
+ **Strengths:**
78
+ - Clear separation between UI (View) and logic (ViewModel)
79
+ - Highly testable - ViewModels are plain Swift classes
80
+ - Industry standard pattern - team members likely know it
81
+ - Works well with dependency injection
82
+ - @Observable (iOS 17+) provides better performance than ObservableObject
83
+ - Scalable to large codebases
84
+
85
+ **Weaknesses:**
86
+ - More boilerplate than native SwiftUI approach
87
+ - MVVM conflicts with SwiftData's @Query requirements
88
+ - Can lead to massive ViewModels if not disciplined
89
+ - Requires iOS 17+ for @Observable benefits
90
+
91
+ **Current status:** Actively used, transitioning from ObservableObject to @Observable
92
+
93
+ **Learning curve:** Medium - familiar to many developers
94
+
95
+ ```swift
96
+ // Model
97
+ struct User: Identifiable, Codable {
98
+ let id: UUID
99
+ let name: String
100
+ let email: String
101
+ }
102
+
103
+ // ViewModel using @Observable (iOS 17+)
104
+ import Observation
105
+
106
+ @Observable
107
+ @MainActor
108
+ class UserListViewModel {
109
+ var users: [User] = []
110
+ var isLoading = false
111
+ var errorMessage: String?
112
+
113
+ private let userService: UserServiceProtocol
114
+
115
+ init(userService: UserServiceProtocol = UserService()) {
116
+ self.userService = userService
117
+ }
118
+
119
+ func loadUsers() async {
120
+ isLoading = true
121
+ errorMessage = nil
122
+
123
+ do {
124
+ users = try await userService.fetchUsers()
125
+ } catch {
126
+ errorMessage = error.localizedDescription
127
+ }
128
+
129
+ isLoading = false
130
+ }
131
+
132
+ func deleteUser(_ user: User) async {
133
+ do {
134
+ try await userService.deleteUser(user.id)
135
+ users.removeAll { $0.id == user.id }
136
+ } catch {
137
+ errorMessage = "Failed to delete user"
138
+ }
139
+ }
140
+ }
141
+
142
+ // View
143
+ struct UserListView: View {
144
+ @State private var viewModel = UserListViewModel()
145
+
146
+ var body: some View {
147
+ List {
148
+ ForEach(viewModel.users) { user in
149
+ UserRowView(user: user)
150
+ }
151
+ .onDelete { indexSet in
152
+ Task {
153
+ for index in indexSet {
154
+ await viewModel.deleteUser(viewModel.users[index])
155
+ }
156
+ }
157
+ }
158
+ }
159
+ .overlay {
160
+ if viewModel.isLoading {
161
+ ProgressView()
162
+ }
163
+ }
164
+ .alert("Error", isPresented: .constant(viewModel.errorMessage != nil)) {
165
+ Button("OK") { viewModel.errorMessage = nil }
166
+ } message: {
167
+ Text(viewModel.errorMessage ?? "")
168
+ }
169
+ .task {
170
+ await viewModel.loadUsers()
171
+ }
172
+ }
173
+ }
174
+
175
+ // Service protocol for dependency injection
176
+ protocol UserServiceProtocol {
177
+ func fetchUsers() async throws -> [User]
178
+ func deleteUser(_ id: UUID) async throws
179
+ }
180
+
181
+ class UserService: UserServiceProtocol {
182
+ func fetchUsers() async throws -> [User] {
183
+ let url = URL(string: "https://api.example.com/users")!
184
+ let (data, _) = try await URLSession.shared.data(from: url)
185
+ return try JSONDecoder().decode([User].self, from: data)
186
+ }
187
+
188
+ func deleteUser(_ id: UUID) async throws {
189
+ let url = URL(string: "https://api.example.com/users/\(id)")!
190
+ var request = URLRequest(url: url)
191
+ request.httpMethod = "DELETE"
192
+ _ = try await URLSession.shared.data(for: request)
193
+ }
194
+ }
195
+ ```
196
+ </option>
197
+
198
+ <option name="The Composable Architecture (TCA)">
199
+ **When to use:** Complex apps with heavy state management, apps requiring time-travel debugging, teams prioritizing testability, apps with complex navigation flows
200
+
201
+ **Strengths:**
202
+ - Unidirectional data flow makes state changes predictable
203
+ - Exceptional testability - reducer tests are pure functions
204
+ - Built-in support for effects and dependencies
205
+ - Time-travel debugging capabilities
206
+ - Strong composition story for large features
207
+ - Stack-based and tree-based navigation support
208
+ - @Shared macro for state sharing across features
209
+
210
+ **Weaknesses:**
211
+ - Steep learning curve - requires learning reducers, stores, actions
212
+ - More boilerplate than MVVM or native approaches
213
+ - Can feel over-engineered for simple features
214
+ - External dependency (not Apple-provided)
215
+ - Scaling issues reported in very large multi-team apps
216
+
217
+ **Current status:** Version 1.13+ (actively maintained by Point-Free, 2024)
218
+
219
+ **Learning curve:** Hard - Redux concepts unfamiliar to many iOS developers
220
+
221
+ ```swift
222
+ import ComposableArchitecture
223
+
224
+ // Feature definition
225
+ @Reducer
226
+ struct UserList {
227
+ @ObservableState
228
+ struct State {
229
+ var users: [User] = []
230
+ var isLoading = false
231
+ var errorMessage: String?
232
+ }
233
+
234
+ enum Action {
235
+ case loadUsers
236
+ case usersResponse(Result<[User], Error>)
237
+ case deleteUser(User)
238
+ case deleteUserResponse(Result<Void, Error>)
239
+ }
240
+
241
+ @Dependency(\.userClient) var userClient
242
+
243
+ var body: some Reducer<State, Action> {
244
+ Reduce { state, action in
245
+ switch action {
246
+ case .loadUsers:
247
+ state.isLoading = true
248
+ state.errorMessage = nil
249
+ return .run { send in
250
+ await send(.usersResponse(
251
+ Result { try await userClient.fetchUsers() }
252
+ ))
253
+ }
254
+
255
+ case let .usersResponse(.success(users)):
256
+ state.isLoading = false
257
+ state.users = users
258
+ return .none
259
+
260
+ case let .usersResponse(.failure(error)):
261
+ state.isLoading = false
262
+ state.errorMessage = error.localizedDescription
263
+ return .none
264
+
265
+ case let .deleteUser(user):
266
+ return .run { send in
267
+ await send(.deleteUserResponse(
268
+ Result { try await userClient.deleteUser(user.id) }
269
+ ))
270
+ }
271
+
272
+ case .deleteUserResponse(.success):
273
+ return .none
274
+
275
+ case let .deleteUserResponse(.failure(error)):
276
+ state.errorMessage = "Failed to delete user"
277
+ return .none
278
+ }
279
+ }
280
+ }
281
+ }
282
+
283
+ // View
284
+ struct UserListView: View {
285
+ let store: StoreOf<UserList>
286
+
287
+ var body: some View {
288
+ List {
289
+ ForEach(store.users) { user in
290
+ UserRowView(user: user)
291
+ }
292
+ .onDelete { indexSet in
293
+ for index in indexSet {
294
+ store.send(.deleteUser(store.users[index]))
295
+ }
296
+ }
297
+ }
298
+ .overlay {
299
+ if store.isLoading {
300
+ ProgressView()
301
+ }
302
+ }
303
+ .alert(
304
+ "Error",
305
+ isPresented: .constant(store.errorMessage != nil)
306
+ ) {
307
+ Button("OK") { }
308
+ } message: {
309
+ Text(store.errorMessage ?? "")
310
+ }
311
+ .task {
312
+ store.send(.loadUsers)
313
+ }
314
+ }
315
+ }
316
+
317
+ // Dependency client
318
+ struct UserClient {
319
+ var fetchUsers: () async throws -> [User]
320
+ var deleteUser: (UUID) async throws -> Void
321
+ }
322
+
323
+ extension UserClient: DependencyKey {
324
+ static let liveValue = UserClient(
325
+ fetchUsers: {
326
+ let url = URL(string: "https://api.example.com/users")!
327
+ let (data, _) = try await URLSession.shared.data(from: url)
328
+ return try JSONDecoder().decode([User].self, from: data)
329
+ },
330
+ deleteUser: { id in
331
+ let url = URL(string: "https://api.example.com/users/\(id)")!
332
+ var request = URLRequest(url: url)
333
+ request.httpMethod = "DELETE"
334
+ _ = try await URLSession.shared.data(for: request)
335
+ }
336
+ )
337
+ }
338
+
339
+ extension DependencyValues {
340
+ var userClient: UserClient {
341
+ get { self[UserClient.self] }
342
+ set { self[UserClient.self] = newValue }
343
+ }
344
+ }
345
+ ```
346
+ </option>
347
+
348
+ <option name="Clean Architecture with Feature Modules">
349
+ **When to use:** Large apps with multiple teams, apps expecting significant future growth, apps requiring strong architectural boundaries, enterprise applications
350
+
351
+ **Strengths:**
352
+ - Enforces clear separation of layers (Presentation, Domain, Data)
353
+ - Modules can be developed and tested independently
354
+ - Faster build times (only changed modules rebuild)
355
+ - Strong architectural boundaries prevent tangled dependencies
356
+ - Works well with Swift Package Manager
357
+ - Teams can work on features autonomously
358
+ - Easy to remove entire features (just delete the package)
359
+
360
+ **Weaknesses:**
361
+ - Significant upfront architectural investment
362
+ - Can be over-engineering for small teams or apps
363
+ - Requires discipline to maintain boundaries
364
+ - More complex dependency management
365
+
366
+ **Current status:** Industry best practice for large-scale apps (2024)
367
+
368
+ **Learning curve:** Hard - requires understanding of layers, boundaries, and modularization
369
+
370
+ ```swift
371
+ // Domain Layer (Core business logic - no framework dependencies)
372
+ // Package: Domain
373
+
374
+ struct User: Identifiable {
375
+ let id: UUID
376
+ let name: String
377
+ let email: String
378
+ }
379
+
380
+ protocol UserRepositoryProtocol {
381
+ func fetchUsers() async throws -> [User]
382
+ func deleteUser(_ id: UUID) async throws
383
+ }
384
+
385
+ // Data Layer (Infrastructure - API, database, etc.)
386
+ // Package: Data
387
+
388
+ import Foundation
389
+
390
+ public class UserRepository: UserRepositoryProtocol {
391
+ private let apiClient: APIClient
392
+
393
+ public init(apiClient: APIClient) {
394
+ self.apiClient = apiClient
395
+ }
396
+
397
+ public func fetchUsers() async throws -> [User] {
398
+ let dtos: [UserDTO] = try await apiClient.get("/users")
399
+ return dtos.map { $0.toDomain() }
400
+ }
401
+
402
+ public func deleteUser(_ id: UUID) async throws {
403
+ try await apiClient.delete("/users/\(id)")
404
+ }
405
+ }
406
+
407
+ struct UserDTO: Codable {
408
+ let id: UUID
409
+ let name: String
410
+ let email: String
411
+
412
+ func toDomain() -> User {
413
+ User(id: id, name: name, email: email)
414
+ }
415
+ }
416
+
417
+ // Presentation Layer (Feature module)
418
+ // Package: Features/UserList
419
+
420
+ import SwiftUI
421
+ import Observation
422
+ import Domain
423
+
424
+ @Observable
425
+ @MainActor
426
+ public class UserListViewModel {
427
+ var users: [User] = []
428
+ var isLoading = false
429
+ var errorMessage: String?
430
+
431
+ private let userRepository: UserRepositoryProtocol
432
+
433
+ public init(userRepository: UserRepositoryProtocol) {
434
+ self.userRepository = userRepository
435
+ }
436
+
437
+ func loadUsers() async {
438
+ isLoading = true
439
+ do {
440
+ users = try await userRepository.fetchUsers()
441
+ } catch {
442
+ errorMessage = error.localizedDescription
443
+ }
444
+ isLoading = false
445
+ }
446
+ }
447
+
448
+ public struct UserListView: View {
449
+ @State private var viewModel: UserListViewModel
450
+
451
+ public init(viewModel: UserListViewModel) {
452
+ self.viewModel = viewModel
453
+ }
454
+
455
+ public var body: some View {
456
+ List(viewModel.users) { user in
457
+ Text(user.name)
458
+ }
459
+ .task {
460
+ await viewModel.loadUsers()
461
+ }
462
+ }
463
+ }
464
+
465
+ // App Layer (Composition root)
466
+ // Package: App
467
+
468
+ import SwiftUI
469
+
470
+ @main
471
+ struct MyApp: App {
472
+ // Dependency container
473
+ private let container = DependencyContainer()
474
+
475
+ var body: some Scene {
476
+ WindowGroup {
477
+ UserListView(
478
+ viewModel: container.makeUserListViewModel()
479
+ )
480
+ }
481
+ }
482
+ }
483
+
484
+ class DependencyContainer {
485
+ private lazy var apiClient = APIClient(baseURL: "https://api.example.com")
486
+
487
+ private lazy var userRepository: UserRepositoryProtocol = {
488
+ UserRepository(apiClient: apiClient)
489
+ }()
490
+
491
+ func makeUserListViewModel() -> UserListViewModel {
492
+ UserListViewModel(userRepository: userRepository)
493
+ }
494
+ }
495
+ ```
496
+ </option>
497
+ </options>
498
+
499
+ <decision_tree>
500
+ ## Choosing the Right Approach
501
+
502
+ **If building a simple app (under 10 screens, minimal business logic):** Use SwiftUI Native because architectural overhead isn't justified and SwiftUI's built-in patterns are sufficient.
503
+
504
+ **If using SwiftData extensively:** Use SwiftUI Native or consider Clean Architecture. Avoid MVVM because @Query requires views to manage data directly, conflicting with ViewModel patterns.
505
+
506
+ **If you need testability and moderate complexity (10-30 screens):** Use MVVM with @Observable because it provides clean separation, is industry-standard, and offers excellent testability with minimal overhead.
507
+
508
+ **If you have complex state management, navigation, or side effects:** Consider The Composable Architecture because its unidirectional data flow and built-in effect handling excel at managing complexity.
509
+
510
+ **If building a large app with multiple teams:** Use Clean Architecture with Feature Modules because it enforces boundaries, enables parallel development, and improves build times through modularization.
511
+
512
+ **If team is unfamiliar with iOS architectures:** Start with MVVM because it's the most widely understood pattern and has abundant learning resources.
513
+
514
+ **If prototyping or validating product-market fit:** Use SwiftUI Native because you can ship fastest and refactor to MVVM or Clean Architecture later when requirements stabilize.
515
+
516
+ **Default recommendation:** MVVM with @Observable for most production apps because it balances simplicity, testability, and scalability. Migrate to Clean Architecture with modules only when team size or app complexity demands it.
517
+
518
+ **Avoid MVVM when:** Using SwiftData heavily, or building very simple apps where the architectural overhead slows development without providing value.
519
+
520
+ **Avoid TCA when:** Team lacks Redux experience, building simple CRUD apps, or working in large multi-team environments where TCA's scaling limitations may surface.
521
+ </decision_tree>
522
+
523
+ <patterns>
524
+ ## Common Patterns
525
+
526
+ <pattern name="Dependency Injection with Factory">
527
+ **Use when:** Need compile-time safe dependency injection without manual container setup
528
+
529
+ Factory is the current recommended DI library for Swift (2024). Import the library as "FactoryKit" to avoid naming conflicts.
530
+
531
+ **Implementation:**
532
+ ```swift
533
+ // 1. Install Factory via SPM
534
+ // https://github.com/hmlongco/Factory
535
+ // Add package, select "FactoryKit" library
536
+
537
+ // 2. Define container with factories
538
+ import FactoryKit
539
+
540
+ extension Container {
541
+ var apiClient: Factory<APIClient> {
542
+ Factory(self) { APIClient(baseURL: "https://api.example.com") }
543
+ }
544
+
545
+ var userRepository: Factory<UserRepositoryProtocol> {
546
+ Factory(self) { UserRepository(apiClient: self.apiClient()) }
547
+ }
548
+
549
+ var userListViewModel: Factory<UserListViewModel> {
550
+ Factory(self) {
551
+ UserListViewModel(userRepository: self.userRepository())
552
+ }
553
+ .scope(.shared) // Singleton if needed
554
+ }
555
+ }
556
+
557
+ // 3. Inject in ViewModels using @Injected
558
+ @Observable
559
+ @MainActor
560
+ class UserListViewModel {
561
+ @ObservationIgnored @Injected(\.userRepository)
562
+ private var userRepository: UserRepositoryProtocol
563
+
564
+ var users: [User] = []
565
+
566
+ func loadUsers() async {
567
+ users = try await userRepository.fetchUsers()
568
+ }
569
+ }
570
+
571
+ // 4. Inject in Views using @Injected
572
+ struct UserListView: View {
573
+ @State private var viewModel = Container.shared.userListViewModel()
574
+
575
+ var body: some View {
576
+ List(viewModel.users) { user in
577
+ Text(user.name)
578
+ }
579
+ }
580
+ }
581
+
582
+ // 5. Override for testing
583
+ extension Container {
584
+ var mockUserRepository: Factory<UserRepositoryProtocol> {
585
+ Factory(self) { MockUserRepository() }
586
+ }
587
+ }
588
+ ```
589
+
590
+ **Considerations:**
591
+ - Use @ObservationIgnored for @Injected properties inside @Observable classes
592
+ - Factory 2.5+ supports Swift 6 strict concurrency
593
+ - Scopes: .singleton, .shared, .cached, .graph, .unique
594
+ - Register mock factories in test targets for easy testing
595
+ </pattern>
596
+
597
+ <pattern name="Environment-Based Dependency Injection">
598
+ **Use when:** Need SwiftUI-native dependency injection without external libraries
599
+
600
+ **Implementation:**
601
+ ```swift
602
+ // 1. Define dependency key
603
+ struct UserRepositoryKey: EnvironmentKey {
604
+ static let defaultValue: UserRepositoryProtocol = UserRepository()
605
+ }
606
+
607
+ extension EnvironmentValues {
608
+ var userRepository: UserRepositoryProtocol {
609
+ get { self[UserRepositoryKey.self] }
610
+ set { self[UserRepositoryKey.self] = newValue }
611
+ }
612
+ }
613
+
614
+ // 2. Provide dependency at app root
615
+ @main
616
+ struct MyApp: App {
617
+ var body: some Scene {
618
+ WindowGroup {
619
+ ContentView()
620
+ .environment(\.userRepository, UserRepository())
621
+ }
622
+ }
623
+ }
624
+
625
+ // 3. Inject in ViewModels
626
+ @Observable
627
+ @MainActor
628
+ class UserListViewModel {
629
+ var users: [User] = []
630
+ private let userRepository: UserRepositoryProtocol
631
+
632
+ init(userRepository: UserRepositoryProtocol) {
633
+ self.userRepository = userRepository
634
+ }
635
+
636
+ func loadUsers() async {
637
+ users = try await userRepository.fetchUsers()
638
+ }
639
+ }
640
+
641
+ // 4. Access in Views
642
+ struct UserListView: View {
643
+ @Environment(\.userRepository) private var userRepository
644
+ @State private var viewModel: UserListViewModel
645
+
646
+ init() {
647
+ // Can't access @Environment in init - use onAppear workaround
648
+ _viewModel = State(initialValue: UserListViewModel(
649
+ userRepository: UserRepository() // temporary
650
+ ))
651
+ }
652
+
653
+ var body: some View {
654
+ List(viewModel.users) { user in
655
+ Text(user.name)
656
+ }
657
+ .onAppear {
658
+ // Replace with environment-injected dependency
659
+ viewModel = UserListViewModel(userRepository: userRepository)
660
+ Task { await viewModel.loadUsers() }
661
+ }
662
+ }
663
+ }
664
+ ```
665
+
666
+ **Considerations:**
667
+ - SwiftUI-native approach (no external dependencies)
668
+ - Can't access @Environment in init - requires workaround
669
+ - Better for simple apps; Factory scales better for complex DI
670
+ </pattern>
671
+
672
+ <pattern name="Repository Pattern for Data Access">
673
+ **Use when:** Abstracting data sources (API, database, cache) from business logic
674
+
675
+ **Implementation:**
676
+ ```swift
677
+ // Protocol in Domain layer
678
+ protocol UserRepositoryProtocol {
679
+ func fetchUsers() async throws -> [User]
680
+ func getUser(id: UUID) async throws -> User
681
+ func saveUser(_ user: User) async throws
682
+ func deleteUser(id: UUID) async throws
683
+ }
684
+
685
+ // Implementation in Data layer
686
+ class UserRepository: UserRepositoryProtocol {
687
+ private let apiClient: APIClient
688
+ private let cache: CacheService
689
+
690
+ init(apiClient: APIClient, cache: CacheService) {
691
+ self.apiClient = apiClient
692
+ self.cache = cache
693
+ }
694
+
695
+ func fetchUsers() async throws -> [User] {
696
+ // Check cache first
697
+ if let cached: [User] = cache.get(key: "users") {
698
+ return cached
699
+ }
700
+
701
+ // Fetch from API
702
+ let dtos: [UserDTO] = try await apiClient.get("/users")
703
+ let users = dtos.map { $0.toDomain() }
704
+
705
+ // Update cache
706
+ cache.set(key: "users", value: users)
707
+
708
+ return users
709
+ }
710
+
711
+ func getUser(id: UUID) async throws -> User {
712
+ let dto: UserDTO = try await apiClient.get("/users/\(id)")
713
+ return dto.toDomain()
714
+ }
715
+
716
+ func saveUser(_ user: User) async throws {
717
+ let dto = UserDTO(from: user)
718
+ try await apiClient.post("/users", body: dto)
719
+
720
+ // Invalidate cache
721
+ cache.remove(key: "users")
722
+ }
723
+
724
+ func deleteUser(id: UUID) async throws {
725
+ try await apiClient.delete("/users/\(id)")
726
+ cache.remove(key: "users")
727
+ }
728
+ }
729
+
730
+ // DTO for API mapping
731
+ struct UserDTO: Codable {
732
+ let id: UUID
733
+ let name: String
734
+ let email: String
735
+
736
+ func toDomain() -> User {
737
+ User(id: id, name: name, email: email)
738
+ }
739
+
740
+ init(from user: User) {
741
+ self.id = user.id
742
+ self.name = user.name
743
+ self.email = user.email
744
+ }
745
+ }
746
+ ```
747
+
748
+ **Considerations:**
749
+ - Repository owns caching strategy
750
+ - DTOs map between API and domain models
751
+ - Protocol enables easy mocking for tests
752
+ - Keeps networking details out of ViewModels
753
+ </pattern>
754
+
755
+ <pattern name="Feature Flag Service">
756
+ **Use when:** Need to toggle features without app updates
757
+
758
+ **Implementation:**
759
+ ```swift
760
+ // Feature flag service
761
+ @Observable
762
+ @MainActor
763
+ class FeatureFlagService {
764
+ private(set) var flags: [String: Bool] = [:]
765
+
766
+ func isEnabled(_ feature: FeatureFlag) -> Bool {
767
+ flags[feature.rawValue] ?? feature.defaultValue
768
+ }
769
+
770
+ func enable(_ feature: FeatureFlag) {
771
+ flags[feature.rawValue] = true
772
+ }
773
+
774
+ func disable(_ feature: FeatureFlag) {
775
+ flags[feature.rawValue] = false
776
+ }
777
+
778
+ func loadRemoteFlags() async {
779
+ // Fetch from remote config service
780
+ // Update flags dictionary
781
+ }
782
+ }
783
+
784
+ enum FeatureFlag: String {
785
+ case newUserProfile = "new_user_profile"
786
+ case darkModeToggle = "dark_mode_toggle"
787
+ case experimentalSearch = "experimental_search"
788
+
789
+ var defaultValue: Bool {
790
+ switch self {
791
+ case .newUserProfile: return false
792
+ case .darkModeToggle: return true
793
+ case .experimentalSearch: return false
794
+ }
795
+ }
796
+ }
797
+
798
+ // Use in views
799
+ struct ContentView: View {
800
+ @Environment(\.featureFlags) private var featureFlags
801
+
802
+ var body: some View {
803
+ VStack {
804
+ if featureFlags.isEnabled(.newUserProfile) {
805
+ NewUserProfileView()
806
+ } else {
807
+ LegacyUserProfileView()
808
+ }
809
+ }
810
+ }
811
+ }
812
+
813
+ // Environment setup
814
+ struct FeatureFlagKey: EnvironmentKey {
815
+ static let defaultValue = FeatureFlagService()
816
+ }
817
+
818
+ extension EnvironmentValues {
819
+ var featureFlags: FeatureFlagService {
820
+ get { self[FeatureFlagKey.self] }
821
+ set { self[FeatureFlagKey.self] = newValue }
822
+ }
823
+ }
824
+ ```
825
+
826
+ **Considerations:**
827
+ - Load remote flags at app startup
828
+ - Use @MainActor for thread-safe access
829
+ - Feature flags enable A/B testing and gradual rollouts
830
+ </pattern>
831
+
832
+ <pattern name="Coordinator Pattern for Navigation">
833
+ **Use when:** Need centralized navigation control separate from views
834
+
835
+ **Implementation:**
836
+ ```swift
837
+ // Navigation coordinator
838
+ @Observable
839
+ @MainActor
840
+ class AppCoordinator {
841
+ var navigationPath = NavigationPath()
842
+ var presentedSheet: SheetDestination?
843
+
844
+ func push(_ destination: Destination) {
845
+ navigationPath.append(destination)
846
+ }
847
+
848
+ func pop() {
849
+ navigationPath.removeLast()
850
+ }
851
+
852
+ func popToRoot() {
853
+ navigationPath = NavigationPath()
854
+ }
855
+
856
+ func present(_ sheet: SheetDestination) {
857
+ presentedSheet = sheet
858
+ }
859
+
860
+ func dismiss() {
861
+ presentedSheet = nil
862
+ }
863
+ }
864
+
865
+ enum Destination: Hashable {
866
+ case userDetail(User)
867
+ case settings
868
+ case editProfile
869
+ }
870
+
871
+ enum SheetDestination: Identifiable {
872
+ case addUser
873
+ case filter
874
+
875
+ var id: String {
876
+ switch self {
877
+ case .addUser: return "addUser"
878
+ case .filter: return "filter"
879
+ }
880
+ }
881
+ }
882
+
883
+ // Root view with coordinator
884
+ struct RootView: View {
885
+ @State private var coordinator = AppCoordinator()
886
+
887
+ var body: some View {
888
+ NavigationStack(path: $coordinator.navigationPath) {
889
+ UserListView(coordinator: coordinator)
890
+ .navigationDestination(for: Destination.self) { destination in
891
+ switch destination {
892
+ case .userDetail(let user):
893
+ UserDetailView(user: user, coordinator: coordinator)
894
+ case .settings:
895
+ SettingsView(coordinator: coordinator)
896
+ case .editProfile:
897
+ EditProfileView(coordinator: coordinator)
898
+ }
899
+ }
900
+ }
901
+ .sheet(item: $coordinator.presentedSheet) { sheet in
902
+ switch sheet {
903
+ case .addUser:
904
+ AddUserView(coordinator: coordinator)
905
+ case .filter:
906
+ FilterView(coordinator: coordinator)
907
+ }
908
+ }
909
+ }
910
+ }
911
+
912
+ // Views use coordinator for navigation
913
+ struct UserListView: View {
914
+ let coordinator: AppCoordinator
915
+
916
+ var body: some View {
917
+ List {
918
+ ForEach(users) { user in
919
+ Button(user.name) {
920
+ coordinator.push(.userDetail(user))
921
+ }
922
+ }
923
+ }
924
+ .toolbar {
925
+ Button("Add") {
926
+ coordinator.present(.addUser)
927
+ }
928
+ }
929
+ }
930
+ }
931
+ ```
932
+
933
+ **Considerations:**
934
+ - Coordinator owns all navigation state
935
+ - Testable - can verify navigation logic independently
936
+ - Works well with deep linking
937
+ - See navigation.md for more patterns
938
+ </pattern>
939
+ </patterns>
940
+
941
+ <anti_patterns>
942
+ ## What NOT to Do
943
+
944
+ <anti_pattern name="Massive ViewModels">
945
+ **Problem:** Putting all feature logic into a single ViewModel class
946
+
947
+ ```swift
948
+ // DON'T: Massive ViewModel with 1000+ lines
949
+ @Observable
950
+ @MainActor
951
+ class UserViewModel {
952
+ // User list logic
953
+ var users: [User] = []
954
+ func loadUsers() async { }
955
+ func deleteUser() { }
956
+
957
+ // User detail logic
958
+ var selectedUser: User?
959
+ func loadUserDetails() { }
960
+
961
+ // Settings logic
962
+ var notificationsEnabled = false
963
+ func saveSettings() { }
964
+
965
+ // Profile editing logic
966
+ var editedName = ""
967
+ var editedEmail = ""
968
+ func updateProfile() { }
969
+
970
+ // Search logic
971
+ var searchQuery = ""
972
+ var searchResults: [User] = []
973
+ func search() { }
974
+
975
+ // ... 900 more lines
976
+ }
977
+ ```
978
+
979
+ **Why it's bad:**
980
+ - Hard to test (must mock entireViewModel for one feature)
981
+ - Poor cohesion (unrelated concerns mixed together)
982
+ - Difficult to navigate and understand
983
+ - High merge conflict risk in teams
984
+
985
+ **Instead:** Create feature-specific ViewModels
986
+
987
+ ```swift
988
+ // DO: Separate ViewModels per feature
989
+ @Observable
990
+ @MainActor
991
+ class UserListViewModel {
992
+ var users: [User] = []
993
+ var isLoading = false
994
+
995
+ func loadUsers() async { }
996
+ func deleteUser(_ user: User) { }
997
+ }
998
+
999
+ @Observable
1000
+ @MainActor
1001
+ class UserDetailViewModel {
1002
+ var user: User
1003
+ var isEditing = false
1004
+
1005
+ init(user: User) {
1006
+ self.user = user
1007
+ }
1008
+
1009
+ func loadDetails() async { }
1010
+ }
1011
+
1012
+ @Observable
1013
+ @MainActor
1014
+ class UserSearchViewModel {
1015
+ var query = ""
1016
+ var results: [User] = []
1017
+
1018
+ func search() async { }
1019
+ }
1020
+ ```
1021
+ </anti_pattern>
1022
+
1023
+ <anti_pattern name="Using ObservableObject Instead of @Observable">
1024
+ **Problem:** Still using the old ObservableObject protocol with @Published
1025
+
1026
+ ```swift
1027
+ // DON'T: Old ObservableObject pattern (pre-iOS 17)
1028
+ class UserViewModel: ObservableObject {
1029
+ @Published var users: [User] = []
1030
+ @Published var isLoading = false
1031
+ @Published var errorMessage: String?
1032
+ }
1033
+
1034
+ struct UserListView: View {
1035
+ @StateObject private var viewModel = UserViewModel()
1036
+ // ...
1037
+ }
1038
+ ```
1039
+
1040
+ **Why it's bad:**
1041
+ - Worse performance (all @Published changes trigger view updates)
1042
+ - More boilerplate (@Published everywhere)
1043
+ - Forces use of @StateObject instead of @State
1044
+ - Triggers unnecessary view redraws
1045
+
1046
+ **Instead:** Use @Observable macro (iOS 17+)
1047
+
1048
+ ```swift
1049
+ // DO: Modern @Observable pattern
1050
+ import Observation
1051
+
1052
+ @Observable
1053
+ @MainActor
1054
+ class UserViewModel {
1055
+ var users: [User] = []
1056
+ var isLoading = false
1057
+ var errorMessage: String?
1058
+ }
1059
+
1060
+ struct UserListView: View {
1061
+ @State private var viewModel = UserViewModel()
1062
+
1063
+ var body: some View {
1064
+ // Only redraws when properties accessed in body change
1065
+ List(viewModel.users) { user in
1066
+ Text(user.name)
1067
+ }
1068
+ }
1069
+ }
1070
+ ```
1071
+
1072
+ **Migration note:** If supporting iOS 16 or earlier, ObservableObject is still required. Use @Observable for iOS 17+ only projects.
1073
+ </anti_pattern>
1074
+
1075
+ <anti_pattern name="Mixing SwiftData @Query with MVVM">
1076
+ **Problem:** Trying to use @Query inside a ViewModel
1077
+
1078
+ ```swift
1079
+ // DON'T: @Query doesn't work in ViewModels
1080
+ @Observable
1081
+ @MainActor
1082
+ class UserViewModel {
1083
+ @Query var users: [User] // ERROR: @Query only works in Views
1084
+ }
1085
+ ```
1086
+
1087
+ **Why it's bad:**
1088
+ - @Query requires SwiftUI view context
1089
+ - Creates compile errors
1090
+ - Forces awkward workarounds
1091
+
1092
+ **Instead:** Use @Query directly in views or avoid MVVM with SwiftData
1093
+
1094
+ ```swift
1095
+ // DO: Use @Query in views directly
1096
+ struct UserListView: View {
1097
+ @Query(sort: \User.name) private var users: [User]
1098
+
1099
+ var body: some View {
1100
+ List(users) { user in
1101
+ Text(user.name)
1102
+ }
1103
+ }
1104
+ }
1105
+
1106
+ // OR: Use ModelContext directly in ViewModel if you need MVVM
1107
+ @Observable
1108
+ @MainActor
1109
+ class UserViewModel {
1110
+ private let modelContext: ModelContext
1111
+ var users: [User] = []
1112
+
1113
+ init(modelContext: ModelContext) {
1114
+ self.modelContext = modelContext
1115
+ }
1116
+
1117
+ func loadUsers() {
1118
+ let descriptor = FetchDescriptor<User>(
1119
+ sortBy: [SortDescriptor(\User.name)]
1120
+ )
1121
+ users = (try? modelContext.fetch(descriptor)) ?? []
1122
+ }
1123
+ }
1124
+ ```
1125
+
1126
+ **Best approach:** If using SwiftData heavily, prefer SwiftUI Native architecture over MVVM.
1127
+ </anti_pattern>
1128
+
1129
+ <anti_pattern name="Initializing @Observable with @State Incorrectly">
1130
+ **Problem:** Creating new ViewModel instance on every view redraw
1131
+
1132
+ ```swift
1133
+ // DON'T: Creates new instance every redraw
1134
+ struct UserListView: View {
1135
+ @State private var viewModel = UserListViewModel() // Wrong!
1136
+
1137
+ var body: some View {
1138
+ List(viewModel.users) { user in
1139
+ Text(user.name)
1140
+ }
1141
+ }
1142
+ }
1143
+ ```
1144
+
1145
+ **Why it seems wrong:** Looks like it creates new instance on every redraw
1146
+
1147
+ **Why it's actually correct:** @State caches the instance across redraws. The initializer only runs on first render.
1148
+
1149
+ **Key insight:** With @Observable and @State, the apparent anti-pattern is actually the correct pattern. Unlike @StateObject (which requires @escaping closure workaround), @State with @Observable caches the value automatically.
1150
+
1151
+ **Still avoid:**
1152
+ ```swift
1153
+ // DON'T: Creating without @State
1154
+ struct UserListView: View {
1155
+ private let viewModel = UserListViewModel() // Wrong - recreated every time
1156
+ }
1157
+
1158
+ // DON'T: Using @StateObject with @Observable
1159
+ struct UserListView: View {
1160
+ @StateObject private var viewModel = UserListViewModel() // Wrong - use @State
1161
+ }
1162
+ ```
1163
+
1164
+ **Do:**
1165
+ ```swift
1166
+ // DO: Use @State with @Observable
1167
+ struct UserListView: View {
1168
+ @State private var viewModel = UserListViewModel()
1169
+ }
1170
+
1171
+ // DO: Or inject from parent
1172
+ struct UserListView: View {
1173
+ let viewModel: UserListViewModel
1174
+ }
1175
+ ```
1176
+ </anti_pattern>
1177
+
1178
+ <anti_pattern name="Passing @Environment to ViewModels in init">
1179
+ **Problem:** Trying to access @Environment values in view initializer
1180
+
1181
+ ```swift
1182
+ // DON'T: Can't access @Environment in init
1183
+ struct UserListView: View {
1184
+ @Environment(\.userRepository) private var userRepository
1185
+ @State private var viewModel: UserListViewModel
1186
+
1187
+ init() {
1188
+ // ERROR: Can't access userRepository here
1189
+ _viewModel = State(initialValue: UserListViewModel(
1190
+ userRepository: userRepository
1191
+ ))
1192
+ }
1193
+ }
1194
+ ```
1195
+
1196
+ **Why it's bad:**
1197
+ - @Environment not available until view is in the hierarchy
1198
+ - Causes compile errors or runtime crashes
1199
+ - Requires awkward workarounds
1200
+
1201
+ **Instead:** Use Factory for DI or pass dependencies explicitly
1202
+
1203
+ ```swift
1204
+ // DO: Use Factory for clean DI
1205
+ import FactoryKit
1206
+
1207
+ extension Container {
1208
+ var userRepository: Factory<UserRepositoryProtocol> {
1209
+ Factory(self) { UserRepository() }
1210
+ }
1211
+ }
1212
+
1213
+ @Observable
1214
+ @MainActor
1215
+ class UserListViewModel {
1216
+ @ObservationIgnored @Injected(\.userRepository)
1217
+ private var userRepository
1218
+ }
1219
+
1220
+ struct UserListView: View {
1221
+ @State private var viewModel = UserListViewModel()
1222
+ // userRepository injected automatically by Factory
1223
+ }
1224
+
1225
+ // OR: Pass from parent that has access
1226
+ struct ParentView: View {
1227
+ @Environment(\.userRepository) private var userRepository
1228
+
1229
+ var body: some View {
1230
+ UserListView(
1231
+ viewModel: UserListViewModel(userRepository: userRepository)
1232
+ )
1233
+ }
1234
+ }
1235
+ ```
1236
+ </anti_pattern>
1237
+
1238
+ <anti_pattern name="God Objects / Service Locators">
1239
+ **Problem:** Creating one massive dependency container that knows about everything
1240
+
1241
+ ```swift
1242
+ // DON'T: God object with all dependencies
1243
+ class AppDependencies {
1244
+ let apiClient: APIClient
1245
+ let userRepository: UserRepository
1246
+ let postRepository: PostRepository
1247
+ let authService: AuthService
1248
+ let cacheService: CacheService
1249
+ let analyticsService: AnalyticsService
1250
+ let pushService: PushService
1251
+ let locationService: LocationService
1252
+ // ... 50 more dependencies
1253
+
1254
+ init() {
1255
+ // Complex initialization graph
1256
+ }
1257
+ }
1258
+
1259
+ // Passed everywhere
1260
+ struct UserListView: View {
1261
+ let dependencies: AppDependencies
1262
+ }
1263
+ ```
1264
+
1265
+ **Why it's bad:**
1266
+ - Views depend on entire app graph (not just what they need)
1267
+ - Hard to test (must construct entire AppDependencies)
1268
+ - Poor encapsulation
1269
+ - Merge conflicts on AppDependencies class
1270
+
1271
+ **Instead:** Use Factory with containers or inject specific dependencies
1272
+
1273
+ ```swift
1274
+ // DO: Factory with automatic resolution
1275
+ extension Container {
1276
+ var userRepository: Factory<UserRepositoryProtocol> {
1277
+ Factory(self) { UserRepository(apiClient: self.apiClient()) }
1278
+ }
1279
+ }
1280
+
1281
+ @Observable
1282
+ @MainActor
1283
+ class UserListViewModel {
1284
+ @ObservationIgnored @Injected(\.userRepository)
1285
+ private var userRepository // Only knows about userRepository
1286
+ }
1287
+
1288
+ // Or inject specific dependencies
1289
+ struct UserListView: View {
1290
+ @State private var viewModel: UserListViewModel
1291
+
1292
+ init(userRepository: UserRepositoryProtocol) {
1293
+ _viewModel = State(initialValue: UserListViewModel(
1294
+ userRepository: userRepository
1295
+ ))
1296
+ }
1297
+ }
1298
+ ```
1299
+ </anti_pattern>
1300
+ </anti_patterns>
1301
+
1302
+ <project_structure>
1303
+ ## Recommended Project Structure
1304
+
1305
+ ### Small Apps (5-15 screens, single developer)
1306
+
1307
+ ```
1308
+ MyApp/
1309
+ ├── MyApp.swift # @main App entry point
1310
+ ├── Models/
1311
+ │ ├── User.swift
1312
+ │ ├── Post.swift
1313
+ │ └── Comment.swift
1314
+ ├── Views/
1315
+ │ ├── UserList/
1316
+ │ │ ├── UserListView.swift
1317
+ │ │ └── UserRowView.swift
1318
+ │ ├── UserDetail/
1319
+ │ │ └── UserDetailView.swift
1320
+ │ └── Settings/
1321
+ │ └── SettingsView.swift
1322
+ ├── ViewModels/ # If using MVVM
1323
+ │ ├── UserListViewModel.swift
1324
+ │ └── UserDetailViewModel.swift
1325
+ ├── Services/
1326
+ │ ├── APIClient.swift
1327
+ │ └── CacheService.swift
1328
+ ├── Utilities/
1329
+ │ ├── Extensions.swift
1330
+ │ └── Constants.swift
1331
+ └── Resources/
1332
+ ├── Assets.xcassets
1333
+ └── Localizable.strings
1334
+ ```
1335
+
1336
+ **Key principles:**
1337
+ - Flat structure with minimal nesting
1338
+ - Group by feature (UserList, UserDetail) not layer
1339
+ - ViewModels folder only if using MVVM
1340
+ - Services for shared business logic
1341
+
1342
+ ### Medium Apps (15-50 screens, 2-5 developers)
1343
+
1344
+ ```
1345
+ MyApp/
1346
+ ├── MyApp.swift
1347
+ ├── App/
1348
+ │ ├── DependencyContainer.swift
1349
+ │ └── AppCoordinator.swift
1350
+ ├── Features/
1351
+ │ ├── Authentication/
1352
+ │ │ ├── Views/
1353
+ │ │ │ ├── LoginView.swift
1354
+ │ │ │ └── SignupView.swift
1355
+ │ │ ├── ViewModels/
1356
+ │ │ │ └── AuthViewModel.swift
1357
+ │ │ └── Models/
1358
+ │ │ └── AuthState.swift
1359
+ │ ├── UserList/
1360
+ │ │ ├── Views/
1361
+ │ │ │ ├── UserListView.swift
1362
+ │ │ │ └── UserRowView.swift
1363
+ │ │ ├── ViewModels/
1364
+ │ │ │ └── UserListViewModel.swift
1365
+ │ │ └── Models/
1366
+ │ │ └── User.swift
1367
+ │ ├── UserDetail/
1368
+ │ │ ├── Views/
1369
+ │ │ ├── ViewModels/
1370
+ │ │ └── Models/
1371
+ │ └── Settings/
1372
+ │ ├── Views/
1373
+ │ └── ViewModels/
1374
+ ├── Core/
1375
+ │ ├── Networking/
1376
+ │ │ ├── APIClient.swift
1377
+ │ │ ├── Endpoint.swift
1378
+ │ │ └── NetworkError.swift
1379
+ │ ├── Persistence/
1380
+ │ │ └── CacheService.swift
1381
+ │ ├── Extensions/
1382
+ │ │ ├── View+Extensions.swift
1383
+ │ │ └── String+Extensions.swift
1384
+ │ └── UI/
1385
+ │ ├── LoadingView.swift
1386
+ │ └── ErrorView.swift
1387
+ └── Resources/
1388
+ ├── Assets.xcassets
1389
+ └── Localizable.strings
1390
+ ```
1391
+
1392
+ **Key principles:**
1393
+ - Features folder with clear feature modules
1394
+ - Each feature has Views/ViewModels/Models subfolders
1395
+ - Core folder for shared infrastructure
1396
+ - App folder for composition root
1397
+ - Still single-target Xcode project
1398
+
1399
+ ### Large Apps (50+ screens, 5+ developers, multi-platform)
1400
+
1401
+ Use Swift Package Manager with modular architecture:
1402
+
1403
+ ```
1404
+ MyApp/
1405
+ ├── App/
1406
+ │ ├── MyApp/
1407
+ │ │ ├── MyApp.swift
1408
+ │ │ ├── DependencyContainer.swift
1409
+ │ │ └── AppCoordinator.swift
1410
+ │ └── MyApp.xcodeproj
1411
+ ├── Packages/
1412
+ │ ├── Domain/
1413
+ │ │ ├── Package.swift
1414
+ │ │ └── Sources/Domain/
1415
+ │ │ ├── Models/
1416
+ │ │ │ ├── User.swift
1417
+ │ │ │ └── Post.swift
1418
+ │ │ └── Repositories/
1419
+ │ │ ├── UserRepositoryProtocol.swift
1420
+ │ │ └── PostRepositoryProtocol.swift
1421
+ │ ├── Data/
1422
+ │ │ ├── Package.swift
1423
+ │ │ └── Sources/Data/
1424
+ │ │ ├── Repositories/
1425
+ │ │ │ ├── UserRepository.swift
1426
+ │ │ │ └── PostRepository.swift
1427
+ │ │ ├── Networking/
1428
+ │ │ │ ├── APIClient.swift
1429
+ │ │ │ └── DTOs/
1430
+ │ │ └── Persistence/
1431
+ │ │ └── CoreDataStack.swift
1432
+ │ ├── FeatureUserList/
1433
+ │ │ ├── Package.swift
1434
+ │ │ └── Sources/FeatureUserList/
1435
+ │ │ ├── Views/
1436
+ │ │ │ ├── UserListView.swift
1437
+ │ │ │ └── UserRowView.swift
1438
+ │ │ └── ViewModels/
1439
+ │ │ └── UserListViewModel.swift
1440
+ │ ├── FeatureUserDetail/
1441
+ │ │ ├── Package.swift
1442
+ │ │ └── Sources/FeatureUserDetail/
1443
+ │ │ └── ...
1444
+ │ ├── FeatureSettings/
1445
+ │ │ ├── Package.swift
1446
+ │ │ └── Sources/FeatureSettings/
1447
+ │ │ └── ...
1448
+ │ ├── CoreUI/
1449
+ │ │ ├── Package.swift
1450
+ │ │ └── Sources/CoreUI/
1451
+ │ │ ├── Components/
1452
+ │ │ │ ├── LoadingView.swift
1453
+ │ │ │ └── ErrorView.swift
1454
+ │ │ ├── Extensions/
1455
+ │ │ └── Theme/
1456
+ │ └── CoreUtilities/
1457
+ │ ├── Package.swift
1458
+ │ └── Sources/CoreUtilities/
1459
+ │ ├── Extensions/
1460
+ │ └── Logging/
1461
+ └── Tests/
1462
+ ├── DomainTests/
1463
+ ├── DataTests/
1464
+ ├── FeatureUserListTests/
1465
+ └── ...
1466
+ ```
1467
+
1468
+ **Package.swift example (FeatureUserList):**
1469
+ ```swift
1470
+ // swift-tools-version: 5.9
1471
+ import PackageDescription
1472
+
1473
+ let package = Package(
1474
+ name: "FeatureUserList",
1475
+ platforms: [.iOS(.v17), .macOS(.v14)],
1476
+ products: [
1477
+ .library(
1478
+ name: "FeatureUserList",
1479
+ targets: ["FeatureUserList"]
1480
+ ),
1481
+ ],
1482
+ dependencies: [
1483
+ .package(path: "../Domain"),
1484
+ .package(path: "../CoreUI"),
1485
+ ],
1486
+ targets: [
1487
+ .target(
1488
+ name: "FeatureUserList",
1489
+ dependencies: [
1490
+ "Domain",
1491
+ "CoreUI",
1492
+ ]
1493
+ ),
1494
+ .testTarget(
1495
+ name: "FeatureUserListTests",
1496
+ dependencies: ["FeatureUserList"]
1497
+ ),
1498
+ ]
1499
+ )
1500
+ ```
1501
+
1502
+ **Dependency hierarchy (bottom to top):**
1503
+ ```
1504
+ App (top level - depends on everything)
1505
+ ├── Feature modules (depend on Domain, CoreUI, CoreUtilities)
1506
+ ├── Data (depends on Domain)
1507
+ ├── Domain (no dependencies - pure Swift)
1508
+ ├── CoreUI (depends on CoreUtilities)
1509
+ └── CoreUtilities (no dependencies - pure Swift)
1510
+ ```
1511
+
1512
+ **Key principles:**
1513
+ - Features are isolated SPM packages
1514
+ - Each package can be opened and worked on independently
1515
+ - Faster builds (only changed packages rebuild)
1516
+ - Domain layer has no framework dependencies (pure Swift)
1517
+ - Data layer implements Domain protocols
1518
+ - Features depend only on Domain (not on each other)
1519
+ - App layer composes everything
1520
+
1521
+ **Benefits:**
1522
+ - Teams work on separate packages with minimal conflicts
1523
+ - Removing features = delete package folder
1524
+ - Faster SwiftUI previews (don't build unrelated code)
1525
+ - Enforced architectural boundaries via dependencies
1526
+ - Testable in isolation
1527
+
1528
+ **When to modularize:**
1529
+ - More than 50 screens
1530
+ - More than 5 developers
1531
+ - Multiple platforms (iOS, macOS, watchOS)
1532
+ - When build times exceed 2-3 minutes
1533
+ </project_structure>
1534
+
1535
+ ## Sources
1536
+
1537
+ - [Hacking with Swift: MVVM in SwiftUI](https://www.hackingwithswift.com/books/ios-swiftui/introducing-mvvm-into-your-swiftui-project)
1538
+ - [Medium: Modern MVVM in SwiftUI 2025](https://medium.com/@minalkewat/modern-mvvm-in-swiftui-2025-the-clean-architecture-youve-been-waiting-for-72a7d576648e)
1539
+ - [SwiftLee: MVVM Architectural Pattern](https://www.avanderlee.com/swiftui/mvvm-architectural-coding-pattern-to-structure-views/)
1540
+ - [Medium: SwiftUI in 2025 - Forget MVVM](https://dimillian.medium.com/swiftui-in-2025-forget-mvvm-262ff2bbd2ed)
1541
+ - [Alexey Naumov: Clean Architecture for SwiftUI](https://nalexn.github.io/clean-architecture-swiftui/)
1542
+ - [Medium: 2025's Best SwiftUI Architecture](https://medium.com/@minalkewat/2025s-best-swiftui-architecture-mvvm-clean-feature-modules-3a369a22858c)
1543
+ - [GitHub: Clean Architecture SwiftUI](https://github.com/nalexn/clean-architecture-swiftui)
1544
+ - [Medium: MVVM with Organized Folder Structures](https://medium.com/@rogeriocpires_128/implementing-mvvm-in-swiftui-with-organized-folder-structures-bc86845eead8)
1545
+ - [mokacoding: Dependency Injection in SwiftUI](https://mokacoding.com/blog/swiftui-dependency-injection/)
1546
+ - [Lucas van Dongen: Managing Dependencies in SwiftUI](https://lucasvandongen.dev/dependency_injection_swift_swiftui.php)
1547
+ - [GitHub: Factory - Swift Dependency Injection](https://github.com/hmlongco/Factory)
1548
+ - [Jesse Squires: @Observable Macro Deep Dive](https://www.jessesquires.com/blog/2024/09/09/swift-observable-macro/)
1549
+ - [SwiftLee: @Observable Performance](https://www.avanderlee.com/swiftui/observable-macro-performance-increase-observableobject/)
1550
+ - [Apple: Migrating to @Observable](https://developer.apple.com/documentation/swiftui/migrating-from-the-observable-object-protocol-to-the-observable-macro)
1551
+ - [Donny Wals: @Observable Explained](https://www.donnywals.com/observable-in-swiftui-explained/)
1552
+ - [Nimble: Modularizing iOS Apps with SwiftUI and SPM](https://nimblehq.co/blog/modern-approach-modularize-ios-swiftui-spm)
1553
+ - [Medium: Building Large-Scale Apps with SwiftUI](https://azamsharp.medium.com/building-large-scale-apps-with-swiftui-a-guide-to-modular-architecture-9c967be13001)
1554
+ - [Medium: Modular SwiftUI Architecture](https://medium.com/@pavel-holec/swiftui-modular-architecture-9bb1647b70b8)
1555
+ - [Better Programming: Factory Dependency Injection](https://betterprogramming.pub/factory-swift-dependency-injection-14da9b2b5d09)
1556
+ - [Lucas van Dongen: DI Frameworks Compared](https://lucasvandongen.dev/di_frameworks_compared.php)
1557
+ - [Medium: App vs Scene Protocol](https://medium.com/@ksjadhav2699/swiftui-app-vs-scene-protocol-1022e655a1fc)
1558
+ - [Swift with Majid: Managing App in SwiftUI](https://swiftwithmajid.com/2020/08/19/managing-app-in-swiftui/)
1559
+ - [GitHub: Swift Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)
1560
+ - [InfoQ: Swift Composable Architecture](https://www.infoq.com/news/2024/08/swift-composable-architecture/)
1561
+ - [Rod Schmidt: TCA 3 Year Experience](https://rodschmidt.com/posts/composable-architecture-experience/)