@aicgen/aicgen 1.0.0-beta.1 → 1.0.0

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 (160) hide show
  1. package/{.claude/guidelines → .agent/rules}/api-design.md +5 -1
  2. package/{.claude/guidelines → .agent/rules}/architecture.md +5 -1
  3. package/{.claude/guidelines → .agent/rules}/best-practices.md +5 -1
  4. package/{.claude/guidelines → .agent/rules}/code-style.md +5 -1
  5. package/{.claude/guidelines → .agent/rules}/design-patterns.md +5 -1
  6. package/{.claude/guidelines → .agent/rules}/devops.md +5 -1
  7. package/{.claude/guidelines → .agent/rules}/error-handling.md +5 -1
  8. package/.agent/rules/instructions.md +28 -0
  9. package/{.claude/guidelines → .agent/rules}/language.md +5 -1
  10. package/{.claude/guidelines → .agent/rules}/performance.md +5 -1
  11. package/{.claude/guidelines → .agent/rules}/security.md +5 -1
  12. package/{.claude/guidelines → .agent/rules}/testing.md +5 -1
  13. package/.agent/workflows/add-documentation.md +10 -0
  14. package/.agent/workflows/generate-integration-tests.md +10 -0
  15. package/.agent/workflows/generate-unit-tests.md +11 -0
  16. package/.agent/workflows/performance-audit.md +11 -0
  17. package/.agent/workflows/refactor-extract-module.md +12 -0
  18. package/.agent/workflows/security-audit.md +12 -0
  19. package/.gemini/instructions.md +4843 -0
  20. package/.vs/ProjectSettings.json +2 -2
  21. package/.vs/VSWorkspaceState.json +15 -15
  22. package/.vs/aicgen.slnx/v18/DocumentLayout.json +53 -53
  23. package/AGENTS.md +9 -11
  24. package/assets/icon.svg +33 -33
  25. package/bun.lock +734 -26
  26. package/{CLAUDE.md → claude.md} +2 -2
  27. package/config.example.yml +129 -0
  28. package/config.yml +38 -0
  29. package/data/architecture/microservices/api-gateway.md +56 -56
  30. package/data/devops/observability.md +73 -73
  31. package/data/guideline-mappings.yml +128 -0
  32. package/data/language/dart/async.md +289 -0
  33. package/data/language/dart/basics.md +280 -0
  34. package/data/language/dart/error-handling.md +355 -0
  35. package/data/language/dart/index.md +10 -0
  36. package/data/language/dart/testing.md +352 -0
  37. package/data/language/swift/basics.md +477 -0
  38. package/data/language/swift/concurrency.md +654 -0
  39. package/data/language/swift/error-handling.md +679 -0
  40. package/data/language/swift/swiftui-mvvm.md +795 -0
  41. package/data/language/swift/testing.md +708 -0
  42. package/data/version.json +10 -8
  43. package/dist/index.js +50153 -28959
  44. package/jest.config.js +46 -0
  45. package/package.json +14 -3
  46. package/.claude/agents/architecture-reviewer.md +0 -88
  47. package/.claude/agents/guideline-checker.md +0 -73
  48. package/.claude/agents/security-auditor.md +0 -108
  49. package/.claude/settings.json +0 -98
  50. package/.claude/settings.local.json +0 -8
  51. package/.eslintrc.json +0 -28
  52. package/.github/workflows/release.yml +0 -180
  53. package/.github/workflows/test.yml +0 -81
  54. package/CONTRIBUTING.md +0 -821
  55. package/dist/commands/init.d.ts +0 -8
  56. package/dist/commands/init.d.ts.map +0 -1
  57. package/dist/commands/init.js +0 -46
  58. package/dist/commands/init.js.map +0 -1
  59. package/dist/config/profiles.d.ts +0 -4
  60. package/dist/config/profiles.d.ts.map +0 -1
  61. package/dist/config/profiles.js +0 -30
  62. package/dist/config/profiles.js.map +0 -1
  63. package/dist/config/settings.d.ts +0 -7
  64. package/dist/config/settings.d.ts.map +0 -1
  65. package/dist/config/settings.js +0 -7
  66. package/dist/config/settings.js.map +0 -1
  67. package/dist/index.d.ts +0 -3
  68. package/dist/index.d.ts.map +0 -1
  69. package/dist/index.js.map +0 -1
  70. package/dist/models/guideline.d.ts +0 -15
  71. package/dist/models/guideline.d.ts.map +0 -1
  72. package/dist/models/guideline.js +0 -2
  73. package/dist/models/guideline.js.map +0 -1
  74. package/dist/models/preference.d.ts +0 -9
  75. package/dist/models/preference.d.ts.map +0 -1
  76. package/dist/models/preference.js +0 -2
  77. package/dist/models/preference.js.map +0 -1
  78. package/dist/models/profile.d.ts +0 -9
  79. package/dist/models/profile.d.ts.map +0 -1
  80. package/dist/models/profile.js +0 -2
  81. package/dist/models/profile.js.map +0 -1
  82. package/dist/models/project.d.ts +0 -13
  83. package/dist/models/project.d.ts.map +0 -1
  84. package/dist/models/project.js +0 -2
  85. package/dist/models/project.js.map +0 -1
  86. package/dist/services/ai/anthropic.d.ts +0 -7
  87. package/dist/services/ai/anthropic.d.ts.map +0 -1
  88. package/dist/services/ai/anthropic.js +0 -39
  89. package/dist/services/ai/anthropic.js.map +0 -1
  90. package/dist/services/generator.d.ts +0 -2
  91. package/dist/services/generator.d.ts.map +0 -1
  92. package/dist/services/generator.js +0 -4
  93. package/dist/services/generator.js.map +0 -1
  94. package/dist/services/learner.d.ts +0 -2
  95. package/dist/services/learner.d.ts.map +0 -1
  96. package/dist/services/learner.js +0 -4
  97. package/dist/services/learner.js.map +0 -1
  98. package/dist/services/scanner.d.ts +0 -3
  99. package/dist/services/scanner.d.ts.map +0 -1
  100. package/dist/services/scanner.js +0 -54
  101. package/dist/services/scanner.js.map +0 -1
  102. package/dist/utils/errors.d.ts +0 -15
  103. package/dist/utils/errors.d.ts.map +0 -1
  104. package/dist/utils/errors.js +0 -27
  105. package/dist/utils/errors.js.map +0 -1
  106. package/dist/utils/file.d.ts +0 -7
  107. package/dist/utils/file.d.ts.map +0 -1
  108. package/dist/utils/file.js +0 -32
  109. package/dist/utils/file.js.map +0 -1
  110. package/dist/utils/logger.d.ts +0 -6
  111. package/dist/utils/logger.d.ts.map +0 -1
  112. package/dist/utils/logger.js +0 -17
  113. package/dist/utils/logger.js.map +0 -1
  114. package/dist/utils/path.d.ts +0 -6
  115. package/dist/utils/path.d.ts.map +0 -1
  116. package/dist/utils/path.js +0 -14
  117. package/dist/utils/path.js.map +0 -1
  118. package/docs/planning/memory-lane.md +0 -83
  119. package/packaging/linux/aicgen.spec +0 -23
  120. package/packaging/linux/control +0 -9
  121. package/packaging/macos/scripts/postinstall +0 -12
  122. package/packaging/windows/setup.nsi +0 -92
  123. package/scripts/add-categories.ts +0 -87
  124. package/scripts/build-binary.ts +0 -46
  125. package/scripts/embed-data.ts +0 -105
  126. package/scripts/generate-version.ts +0 -150
  127. package/scripts/test-decompress.ts +0 -27
  128. package/scripts/test-extract.ts +0 -31
  129. package/src/__tests__/services/assistant-file-writer.test.ts +0 -400
  130. package/src/__tests__/services/guideline-loader.test.ts +0 -281
  131. package/src/__tests__/services/tarball-extraction.test.ts +0 -125
  132. package/src/commands/add-guideline.ts +0 -296
  133. package/src/commands/clear.ts +0 -61
  134. package/src/commands/guideline-selector.ts +0 -123
  135. package/src/commands/init.ts +0 -645
  136. package/src/commands/quick-add.ts +0 -586
  137. package/src/commands/remove-guideline.ts +0 -152
  138. package/src/commands/stats.ts +0 -49
  139. package/src/commands/update.ts +0 -240
  140. package/src/config.ts +0 -82
  141. package/src/embedded-data.ts +0 -1492
  142. package/src/index.ts +0 -67
  143. package/src/models/profile.ts +0 -24
  144. package/src/models/project.ts +0 -43
  145. package/src/services/assistant-file-writer.ts +0 -612
  146. package/src/services/config-generator.ts +0 -150
  147. package/src/services/config-manager.ts +0 -70
  148. package/src/services/data-source.ts +0 -248
  149. package/src/services/first-run-init.ts +0 -148
  150. package/src/services/guideline-loader.ts +0 -311
  151. package/src/services/hook-generator.ts +0 -178
  152. package/src/services/subagent-generator.ts +0 -310
  153. package/src/utils/banner.ts +0 -66
  154. package/src/utils/errors.ts +0 -27
  155. package/src/utils/file.ts +0 -67
  156. package/src/utils/formatting.ts +0 -172
  157. package/src/utils/logger.ts +0 -89
  158. package/src/utils/path.ts +0 -17
  159. package/src/utils/wizard-state.ts +0 -132
  160. package/tsconfig.json +0 -25
@@ -0,0 +1,795 @@
1
+ # SwiftUI and MVVM Architecture
2
+
3
+ ## MVVM Pattern in SwiftUI
4
+
5
+ SwiftUI works naturally with the MVVM (Model-View-ViewModel) pattern:
6
+
7
+ ```swift
8
+ // Model - Data and business logic
9
+ struct User: Identifiable, Codable {
10
+ let id: String
11
+ var name: String
12
+ var email: String
13
+ var isActive: Bool
14
+ }
15
+
16
+ // ViewModel - Presentation logic
17
+ @MainActor
18
+ class UserViewModel: ObservableObject {
19
+ @Published var users: [User] = []
20
+ @Published var isLoading = false
21
+ @Published var errorMessage: String?
22
+
23
+ private let repository: UserRepository
24
+
25
+ init(repository: UserRepository = UserRepository()) {
26
+ self.repository = repository
27
+ }
28
+
29
+ func loadUsers() async {
30
+ isLoading = true
31
+ errorMessage = nil
32
+
33
+ do {
34
+ users = try await repository.fetchUsers()
35
+ } catch {
36
+ errorMessage = "Failed to load users: \(error.localizedDescription)"
37
+ }
38
+
39
+ isLoading = false
40
+ }
41
+
42
+ func deleteUser(_ user: User) async {
43
+ do {
44
+ try await repository.delete(user)
45
+ users.removeAll { $0.id == user.id }
46
+ } catch {
47
+ errorMessage = "Failed to delete user: \(error.localizedDescription)"
48
+ }
49
+ }
50
+ }
51
+
52
+ // View - UI and user interaction
53
+ struct UserListView: View {
54
+ @StateObject private var viewModel = UserViewModel()
55
+
56
+ var body: some View {
57
+ NavigationView {
58
+ Group {
59
+ if viewModel.isLoading {
60
+ ProgressView("Loading users...")
61
+ } else if let error = viewModel.errorMessage {
62
+ ErrorView(message: error) {
63
+ Task { await viewModel.loadUsers() }
64
+ }
65
+ } else {
66
+ userList
67
+ }
68
+ }
69
+ .navigationTitle("Users")
70
+ .task {
71
+ await viewModel.loadUsers()
72
+ }
73
+ }
74
+ }
75
+
76
+ private var userList: some View {
77
+ List {
78
+ ForEach(viewModel.users) { user in
79
+ UserRow(user: user)
80
+ }
81
+ .onDelete { indexSet in
82
+ Task {
83
+ for index in indexSet {
84
+ await viewModel.deleteUser(viewModel.users[index])
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ ## State Management
94
+
95
+ ### @State for View-Local State
96
+
97
+ ```swift
98
+ // ✅ Use @State for simple view-specific state
99
+ struct CounterView: View {
100
+ @State private var count = 0
101
+
102
+ var body: some View {
103
+ VStack {
104
+ Text("Count: \(count)")
105
+ Button("Increment") {
106
+ count += 1
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ // ❌ Don't use @State for complex or shared state
113
+ struct BadView: View {
114
+ @State private var users: [User] = [] // Should be in ViewModel
115
+ @State private var repository = UserRepository() // Should be injected
116
+ }
117
+ ```
118
+
119
+ ### @StateObject for ViewModel Ownership
120
+
121
+ ```swift
122
+ // ✅ Use @StateObject when view creates and owns the ViewModel
123
+ struct UserListView: View {
124
+ @StateObject private var viewModel = UserViewModel()
125
+
126
+ var body: some View {
127
+ List(viewModel.users) { user in
128
+ Text(user.name)
129
+ }
130
+ .task {
131
+ await viewModel.loadUsers()
132
+ }
133
+ }
134
+ }
135
+
136
+ // ✅ StateObject with dependency injection
137
+ struct UserListView: View {
138
+ @StateObject private var viewModel: UserViewModel
139
+
140
+ init(repository: UserRepository) {
141
+ _viewModel = StateObject(wrappedValue: UserViewModel(repository: repository))
142
+ }
143
+ }
144
+ ```
145
+
146
+ ### @ObservedObject for Passed ViewModels
147
+
148
+ ```swift
149
+ // ✅ Use @ObservedObject when ViewModel is passed from parent
150
+ struct UserDetailView: View {
151
+ @ObservedObject var viewModel: UserDetailViewModel
152
+
153
+ var body: some View {
154
+ VStack {
155
+ Text(viewModel.user.name)
156
+ TextField("Name", text: $viewModel.editableName)
157
+ }
158
+ }
159
+ }
160
+
161
+ // Parent view creates and passes ViewModel
162
+ struct ParentView: View {
163
+ @StateObject private var detailViewModel = UserDetailViewModel()
164
+
165
+ var body: some View {
166
+ UserDetailView(viewModel: detailViewModel)
167
+ }
168
+ }
169
+ ```
170
+
171
+ ### @EnvironmentObject for App-Wide State
172
+
173
+ ```swift
174
+ // ✅ Use @EnvironmentObject for app-wide dependencies
175
+ class AppState: ObservableObject {
176
+ @Published var currentUser: User?
177
+ @Published var isAuthenticated = false
178
+
179
+ func login(user: User) {
180
+ currentUser = user
181
+ isAuthenticated = true
182
+ }
183
+
184
+ func logout() {
185
+ currentUser = nil
186
+ isAuthenticated = false
187
+ }
188
+ }
189
+
190
+ // Inject at app level
191
+ @main
192
+ struct MyApp: App {
193
+ @StateObject private var appState = AppState()
194
+
195
+ var body: some Scene {
196
+ WindowGroup {
197
+ ContentView()
198
+ .environmentObject(appState)
199
+ }
200
+ }
201
+ }
202
+
203
+ // Access in any view
204
+ struct ProfileView: View {
205
+ @EnvironmentObject var appState: AppState
206
+
207
+ var body: some View {
208
+ if let user = appState.currentUser {
209
+ Text("Welcome, \(user.name)")
210
+ Button("Logout") {
211
+ appState.logout()
212
+ }
213
+ }
214
+ }
215
+ }
216
+ ```
217
+
218
+ ### @Binding for Two-Way Data Flow
219
+
220
+ ```swift
221
+ // ✅ Use @Binding for child views that need to modify parent state
222
+ struct ToggleRow: View {
223
+ let title: String
224
+ @Binding var isOn: Bool
225
+
226
+ var body: some View {
227
+ Toggle(title, isOn: $isOn)
228
+ }
229
+ }
230
+
231
+ // Parent provides the binding
232
+ struct SettingsView: View {
233
+ @State private var notificationsEnabled = false
234
+
235
+ var body: some View {
236
+ ToggleRow(title: "Notifications", isOn: $notificationsEnabled)
237
+ }
238
+ }
239
+ ```
240
+
241
+ ## View Composition
242
+
243
+ ### Extract Subviews
244
+
245
+ ```swift
246
+ // ❌ Monolithic view
247
+ struct ProductView: View {
248
+ let product: Product
249
+
250
+ var body: some View {
251
+ VStack {
252
+ HStack {
253
+ AsyncImage(url: product.imageURL) { image in
254
+ image.resizable().aspectRatio(contentMode: .fit)
255
+ } placeholder: {
256
+ ProgressView()
257
+ }
258
+ .frame(width: 60, height: 60)
259
+
260
+ VStack(alignment: .leading) {
261
+ Text(product.name).font(.headline)
262
+ Text(product.description).font(.subheadline).foregroundColor(.gray)
263
+ }
264
+ }
265
+
266
+ HStack {
267
+ Text("$\(product.price, specifier: "%.2f")")
268
+ Spacer()
269
+ if product.inStock {
270
+ Text("In Stock").foregroundColor(.green)
271
+ } else {
272
+ Text("Out of Stock").foregroundColor(.red)
273
+ }
274
+ }
275
+ }
276
+ }
277
+ }
278
+
279
+ // ✅ Composed from smaller views
280
+ struct ProductView: View {
281
+ let product: Product
282
+
283
+ var body: some View {
284
+ VStack(alignment: .leading, spacing: 12) {
285
+ ProductHeader(product: product)
286
+ ProductFooter(product: product)
287
+ }
288
+ }
289
+ }
290
+
291
+ struct ProductHeader: View {
292
+ let product: Product
293
+
294
+ var body: some View {
295
+ HStack {
296
+ ProductImage(url: product.imageURL)
297
+ ProductInfo(name: product.name, description: product.description)
298
+ }
299
+ }
300
+ }
301
+
302
+ struct ProductFooter: View {
303
+ let product: Product
304
+
305
+ var body: some View {
306
+ HStack {
307
+ PriceLabel(price: product.price)
308
+ Spacer()
309
+ StockStatus(inStock: product.inStock)
310
+ }
311
+ }
312
+ }
313
+ ```
314
+
315
+ ### Use ViewBuilder
316
+
317
+ ```swift
318
+ // ✅ ViewBuilder for conditional content
319
+ struct ContentView: View {
320
+ let isLoggedIn: Bool
321
+
322
+ var body: some View {
323
+ VStack {
324
+ headerContent
325
+ mainContent
326
+ }
327
+ }
328
+
329
+ @ViewBuilder
330
+ private var headerContent: some View {
331
+ if isLoggedIn {
332
+ ProfileHeader()
333
+ } else {
334
+ LoginPrompt()
335
+ }
336
+ }
337
+
338
+ @ViewBuilder
339
+ private var mainContent: some View {
340
+ Text("Main Content")
341
+ }
342
+ }
343
+ ```
344
+
345
+ ## Navigation Patterns
346
+
347
+ ### NavigationStack (iOS 16+)
348
+
349
+ ```swift
350
+ // ✅ Type-safe navigation with NavigationStack
351
+ struct NavigationExample: View {
352
+ @State private var path = NavigationPath()
353
+
354
+ var body: some View {
355
+ NavigationStack(path: $path) {
356
+ List(users) { user in
357
+ NavigationLink(value: user) {
358
+ UserRow(user: user)
359
+ }
360
+ }
361
+ .navigationDestination(for: User.self) { user in
362
+ UserDetailView(user: user)
363
+ }
364
+ .navigationTitle("Users")
365
+ }
366
+ }
367
+ }
368
+
369
+ // ✅ Programmatic navigation
370
+ struct ProgrammaticNav: View {
371
+ @State private var path = NavigationPath()
372
+
373
+ var body: some View {
374
+ NavigationStack(path: $path) {
375
+ Button("Go to Detail") {
376
+ path.append(DetailRoute.userDetail(id: "123"))
377
+ }
378
+ .navigationDestination(for: DetailRoute.self) { route in
379
+ destinationView(for: route)
380
+ }
381
+ }
382
+ }
383
+
384
+ @ViewBuilder
385
+ func destinationView(for route: DetailRoute) -> some View {
386
+ switch route {
387
+ case .userDetail(let id):
388
+ UserDetailView(userId: id)
389
+ case .settings:
390
+ SettingsView()
391
+ }
392
+ }
393
+ }
394
+
395
+ enum DetailRoute: Hashable {
396
+ case userDetail(id: String)
397
+ case settings
398
+ }
399
+ ```
400
+
401
+ ### Sheet Presentation
402
+
403
+ ```swift
404
+ // ✅ Present sheet with binding
405
+ struct SheetExample: View {
406
+ @State private var showingSheet = false
407
+ @State private var selectedUser: User?
408
+
409
+ var body: some View {
410
+ List(users) { user in
411
+ Button(user.name) {
412
+ selectedUser = user
413
+ showingSheet = true
414
+ }
415
+ }
416
+ .sheet(isPresented: $showingSheet) {
417
+ if let user = selectedUser {
418
+ UserDetailView(user: user)
419
+ }
420
+ }
421
+ }
422
+ }
423
+
424
+ // ✅ Use item-based sheet (cleaner)
425
+ struct ItemSheetExample: View {
426
+ @State private var selectedUser: User?
427
+
428
+ var body: some View {
429
+ List(users) { user in
430
+ Button(user.name) {
431
+ selectedUser = user
432
+ }
433
+ }
434
+ .sheet(item: $selectedUser) { user in
435
+ UserDetailView(user: user)
436
+ }
437
+ }
438
+ }
439
+ ```
440
+
441
+ ## Data Flow Patterns
442
+
443
+ ### Unidirectional Data Flow
444
+
445
+ ```swift
446
+ // ✅ Data flows down, events flow up
447
+ struct ParentView: View {
448
+ @StateObject private var viewModel = ParentViewModel()
449
+
450
+ var body: some View {
451
+ VStack {
452
+ // Data flows down
453
+ ChildView(
454
+ data: viewModel.data,
455
+ // Events flow up
456
+ onAction: { action in
457
+ viewModel.handle(action)
458
+ }
459
+ )
460
+ }
461
+ }
462
+ }
463
+
464
+ struct ChildView: View {
465
+ let data: String
466
+ let onAction: (Action) -> Void
467
+
468
+ var body: some View {
469
+ Button(data) {
470
+ onAction(.buttonTapped) // Event flows up
471
+ }
472
+ }
473
+ }
474
+
475
+ enum Action {
476
+ case buttonTapped
477
+ case itemSelected(id: String)
478
+ }
479
+ ```
480
+
481
+ ### Combine Integration
482
+
483
+ ```swift
484
+ // ✅ Use Combine for reactive updates
485
+ @MainActor
486
+ class SearchViewModel: ObservableObject {
487
+ @Published var searchText = ""
488
+ @Published var results: [SearchResult] = []
489
+ @Published var isSearching = false
490
+
491
+ private var cancellables = Set<AnyCancellable>()
492
+ private let searchService: SearchService
493
+
494
+ init(searchService: SearchService = SearchService()) {
495
+ self.searchService = searchService
496
+
497
+ $searchText
498
+ .debounce(for: .milliseconds(300), scheduler: DispatchQueue.main)
499
+ .removeDuplicates()
500
+ .sink { [weak self] query in
501
+ Task {
502
+ await self?.search(query: query)
503
+ }
504
+ }
505
+ .store(in: &cancellables)
506
+ }
507
+
508
+ private func search(query: String) async {
509
+ guard !query.isEmpty else {
510
+ results = []
511
+ return
512
+ }
513
+
514
+ isSearching = true
515
+ defer { isSearching = false }
516
+
517
+ do {
518
+ results = try await searchService.search(query: query)
519
+ } catch {
520
+ results = []
521
+ }
522
+ }
523
+ }
524
+ ```
525
+
526
+ ## Custom View Modifiers
527
+
528
+ ```swift
529
+ // ✅ Reusable styling with ViewModifier
530
+ struct CardStyle: ViewModifier {
531
+ func body(content: Content) -> some View {
532
+ content
533
+ .padding()
534
+ .background(Color.white)
535
+ .cornerRadius(12)
536
+ .shadow(radius: 4)
537
+ }
538
+ }
539
+
540
+ extension View {
541
+ func cardStyle() -> some View {
542
+ modifier(CardStyle())
543
+ }
544
+ }
545
+
546
+ // Usage
547
+ Text("Hello")
548
+ .cardStyle()
549
+
550
+ // ✅ Parameterized modifiers
551
+ struct PrimaryButtonStyle: ViewModifier {
552
+ let isEnabled: Bool
553
+
554
+ func body(content: Content) -> some View {
555
+ content
556
+ .font(.headline)
557
+ .foregroundColor(.white)
558
+ .padding()
559
+ .background(isEnabled ? Color.blue : Color.gray)
560
+ .cornerRadius(8)
561
+ }
562
+ }
563
+
564
+ extension View {
565
+ func primaryButton(enabled: Bool = true) -> some View {
566
+ modifier(PrimaryButtonStyle(isEnabled: enabled))
567
+ }
568
+ }
569
+ ```
570
+
571
+ ## Performance Optimization
572
+
573
+ ### Avoid Expensive Computations in body
574
+
575
+ ```swift
576
+ // ❌ Expensive work in body (called on every update)
577
+ struct BadView: View {
578
+ let items: [Item]
579
+
580
+ var body: some View {
581
+ let processed = processExpensiveData(items) // Recomputed every render!
582
+ List(processed) { item in
583
+ Text(item.name)
584
+ }
585
+ }
586
+ }
587
+
588
+ // ✅ Compute in ViewModel or use computed property
589
+ @MainActor
590
+ class GoodViewModel: ObservableObject {
591
+ @Published var items: [Item] = []
592
+
593
+ var processedItems: [ProcessedItem] {
594
+ processExpensiveData(items)
595
+ }
596
+ }
597
+
598
+ // ✅ Or memoize the result
599
+ struct GoodView: View {
600
+ let items: [Item]
601
+
602
+ private var processedItems: [ProcessedItem] {
603
+ // Only recomputed when items change
604
+ items.map { ProcessedItem($0) }
605
+ }
606
+
607
+ var body: some View {
608
+ List(processedItems) { item in
609
+ Text(item.name)
610
+ }
611
+ }
612
+ }
613
+ ```
614
+
615
+ ### Use Equatable for Performance
616
+
617
+ ```swift
618
+ // ✅ Conform to Equatable to avoid unnecessary updates
619
+ struct UserRow: View, Equatable {
620
+ let user: User
621
+
622
+ var body: some View {
623
+ HStack {
624
+ AsyncImage(url: user.avatarURL)
625
+ Text(user.name)
626
+ }
627
+ }
628
+
629
+ static func == (lhs: UserRow, rhs: UserRow) -> Bool {
630
+ lhs.user.id == rhs.user.id
631
+ }
632
+ }
633
+
634
+ // Use .equatable() to enable optimization
635
+ List(users) { user in
636
+ UserRow(user: user)
637
+ .equatable()
638
+ }
639
+ ```
640
+
641
+ ## Testing ViewModels
642
+
643
+ ```swift
644
+ // ✅ Testable ViewModel with dependency injection
645
+ @MainActor
646
+ class UserViewModel: ObservableObject {
647
+ @Published var users: [User] = []
648
+ private let repository: UserRepositoryProtocol
649
+
650
+ init(repository: UserRepositoryProtocol) {
651
+ self.repository = repository
652
+ }
653
+
654
+ func loadUsers() async {
655
+ users = try? await repository.fetchUsers()
656
+ }
657
+ }
658
+
659
+ // Test with mock repository
660
+ @MainActor
661
+ class UserViewModelTests: XCTestCase {
662
+ func testLoadUsers() async {
663
+ let mockRepo = MockUserRepository()
664
+ mockRepo.usersToReturn = [
665
+ User(id: "1", name: "Alice"),
666
+ User(id: "2", name: "Bob")
667
+ ]
668
+
669
+ let viewModel = UserViewModel(repository: mockRepo)
670
+ await viewModel.loadUsers()
671
+
672
+ XCTAssertEqual(viewModel.users.count, 2)
673
+ XCTAssertEqual(viewModel.users.first?.name, "Alice")
674
+ }
675
+ }
676
+ ```
677
+
678
+ ## Best Practices
679
+
680
+ ### Keep Views Simple
681
+
682
+ ```swift
683
+ // ✅ View only contains UI logic
684
+ struct ProductView: View {
685
+ @ObservedObject var viewModel: ProductViewModel
686
+
687
+ var body: some View {
688
+ VStack {
689
+ Text(viewModel.productName)
690
+ Text(viewModel.formattedPrice)
691
+ Button("Add to Cart") {
692
+ Task { await viewModel.addToCart() }
693
+ }
694
+ }
695
+ }
696
+ }
697
+
698
+ // ViewModel contains all business logic
699
+ @MainActor
700
+ class ProductViewModel: ObservableObject {
701
+ @Published var product: Product
702
+
703
+ var productName: String { product.name }
704
+ var formattedPrice: String { "$\(product.price, specifier: "%.2f")" }
705
+
706
+ func addToCart() async {
707
+ // Business logic here
708
+ }
709
+ }
710
+ ```
711
+
712
+ ### Use Preview Provider
713
+
714
+ ```swift
715
+ // ✅ Provide previews for development
716
+ struct UserListView_Previews: PreviewProvider {
717
+ static var previews: some View {
718
+ Group {
719
+ // Default state
720
+ UserListView()
721
+ .previewDisplayName("Default")
722
+
723
+ // Loading state
724
+ UserListView(viewModel: .loading)
725
+ .previewDisplayName("Loading")
726
+
727
+ // Error state
728
+ UserListView(viewModel: .error)
729
+ .previewDisplayName("Error")
730
+ }
731
+ }
732
+ }
733
+
734
+ // Helper to create preview ViewModels
735
+ extension UserViewModel {
736
+ static var loading: UserViewModel {
737
+ let vm = UserViewModel()
738
+ vm.isLoading = true
739
+ return vm
740
+ }
741
+
742
+ static var error: UserViewModel {
743
+ let vm = UserViewModel()
744
+ vm.errorMessage = "Failed to load users"
745
+ return vm
746
+ }
747
+ }
748
+ ```
749
+
750
+ ### Environment for Dependencies
751
+
752
+ ```swift
753
+ // ✅ Use Environment for injecting dependencies
754
+ private struct RepositoryKey: EnvironmentKey {
755
+ static let defaultValue: UserRepository = UserRepository()
756
+ }
757
+
758
+ extension EnvironmentValues {
759
+ var userRepository: UserRepository {
760
+ get { self[RepositoryKey.self] }
761
+ set { self[RepositoryKey.self] = newValue }
762
+ }
763
+ }
764
+
765
+ // Provide at app level
766
+ @main
767
+ struct MyApp: App {
768
+ let repository = UserRepository()
769
+
770
+ var body: some Scene {
771
+ WindowGroup {
772
+ ContentView()
773
+ .environment(\.userRepository, repository)
774
+ }
775
+ }
776
+ }
777
+
778
+ // Access in views
779
+ struct UserListView: View {
780
+ @Environment(\.userRepository) var repository
781
+ @StateObject private var viewModel: UserViewModel
782
+
783
+ init() {
784
+ _viewModel = StateObject(wrappedValue: UserViewModel(repository: repository))
785
+ }
786
+ }
787
+ ```
788
+
789
+ ---
790
+
791
+ **Sources:**
792
+ - [Apple SwiftUI Documentation](https://developer.apple.com/documentation/swiftui/)
793
+ - [SwiftUI Data Flow](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
794
+ - [WWDC22: The SwiftUI cookbook for navigation](https://developer.apple.com/videos/play/wwdc2022/10054/)
795
+ - [Swift by Sundell: SwiftUI Architecture](https://www.swiftbysundell.com/basics/swiftui/)