swift-code-reviewer-skill 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.
@@ -0,0 +1,909 @@
1
+ # SwiftUI Review Checklist
2
+
3
+ This checklist covers SwiftUI-specific patterns including state management, property wrappers, modern API usage, view composition, and accessibility. Use this to ensure SwiftUI code follows best practices and leverages modern APIs effectively.
4
+
5
+ ---
6
+
7
+ ## 1. State Management
8
+
9
+ ### 1.1 @Observable (iOS 17+, macOS 14+)
10
+
11
+ **Check for:**
12
+ - [ ] @Observable used for view models and observable objects
13
+ - [ ] No mixing @Observable with @StateObject/@ObservedObject
14
+ - [ ] Proper state isolation
15
+
16
+ **Examples:**
17
+
18
+ ❌ **Bad: Using old ObservableObject pattern**
19
+ ```swift
20
+ class LoginViewModel: ObservableObject { // ❌ Old pattern (iOS 17+)
21
+ @Published var email: String = ""
22
+ @Published var isLoading: Bool = false
23
+ }
24
+
25
+ struct LoginView: View {
26
+ @StateObject private var viewModel = LoginViewModel() // ❌ Old pattern
27
+ }
28
+ ```
29
+
30
+ ✅ **Good: Modern @Observable pattern**
31
+ ```swift
32
+ @Observable
33
+ final class LoginViewModel { // ✅ Modern pattern (iOS 17+)
34
+ var email: String = ""
35
+ var isLoading: Bool = false
36
+
37
+ func login() async {
38
+ isLoading = true
39
+ // Login logic
40
+ isLoading = false
41
+ }
42
+ }
43
+
44
+ struct LoginView: View {
45
+ let viewModel: LoginViewModel // ✅ No property wrapper needed
46
+
47
+ var body: some View {
48
+ // Automatically observes viewModel changes
49
+ }
50
+ }
51
+ ```
52
+
53
+ ✅ **Good: @Observable with MainActor**
54
+ ```swift
55
+ @MainActor
56
+ @Observable
57
+ final class UserListViewModel { // ✅ MainActor + Observable
58
+ var users: [User] = []
59
+ var isLoading: Bool = false
60
+
61
+ func fetchUsers() async {
62
+ // Always runs on main actor
63
+ }
64
+ }
65
+ ```
66
+
67
+ ### 1.2 @State for View-Local State
68
+
69
+ **Check for:**
70
+ - [ ] @State used only for view-owned state
71
+ - [ ] Private @State properties
72
+ - [ ] No @State for passed data
73
+
74
+ **Examples:**
75
+
76
+ ❌ **Bad: @State for passed data**
77
+ ```swift
78
+ struct UserDetailView: View {
79
+ @State var user: User // ❌ User should be passed as let
80
+
81
+ var body: some View {
82
+ // ...
83
+ }
84
+ }
85
+ ```
86
+
87
+ ✅ **Good: @State for view-local state**
88
+ ```swift
89
+ struct UserDetailView: View {
90
+ let user: User // ✅ Passed data as let
91
+
92
+ @State private var isExpanded: Bool = false // ✅ View-local state
93
+ @State private var selectedTab: Tab = .profile
94
+
95
+ var body: some View {
96
+ VStack {
97
+ Button(isExpanded ? "Collapse" : "Expand") {
98
+ isExpanded.toggle() // ✅ Modifying view-local state
99
+ }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### 1.3 @Binding for Two-Way Communication
106
+
107
+ **Check for:**
108
+ - [ ] @Binding used for child-to-parent communication
109
+ - [ ] Parent owns the state, child has @Binding
110
+ - [ ] No @Binding for read-only data
111
+
112
+ **Examples:**
113
+
114
+ ❌ **Bad: Passing @State directly**
115
+ ```swift
116
+ struct ParentView: View {
117
+ @State private var text: String = ""
118
+
119
+ var body: some View {
120
+ ChildView(text: text) // ❌ Child can't modify
121
+ }
122
+ }
123
+
124
+ struct ChildView: View {
125
+ let text: String
126
+ }
127
+ ```
128
+
129
+ ✅ **Good: Using @Binding for two-way communication**
130
+ ```swift
131
+ struct ParentView: View {
132
+ @State private var text: String = "" // ✅ Parent owns state
133
+
134
+ var body: some View {
135
+ ChildView(text: $text) // ✅ Pass binding with $
136
+ }
137
+ }
138
+
139
+ struct ChildView: View {
140
+ @Binding var text: String // ✅ Child can read and write
141
+
142
+ var body: some View {
143
+ TextField("Enter text", text: $text)
144
+ }
145
+ }
146
+ ```
147
+
148
+ ✅ **Good: Read-only without @Binding**
149
+ ```swift
150
+ struct DisplayView: View {
151
+ let text: String // ✅ Read-only, no @Binding
152
+
153
+ var body: some View {
154
+ Text(text)
155
+ }
156
+ }
157
+ ```
158
+
159
+ ### 1.4 @Environment for Dependency Injection
160
+
161
+ **Check for:**
162
+ - [ ] @Environment for cross-cutting concerns
163
+ - [ ] Custom environment values for dependencies
164
+ - [ ] No direct service access in views
165
+
166
+ **Examples:**
167
+
168
+ ✅ **Good: Custom environment value**
169
+ ```swift
170
+ // Define environment key
171
+ private struct AuthServiceKey: EnvironmentKey {
172
+ static let defaultValue: AuthService = DefaultAuthService()
173
+ }
174
+
175
+ extension EnvironmentValues {
176
+ var authService: AuthService {
177
+ get { self[AuthServiceKey.self] }
178
+ set { self[AuthServiceKey.self] = newValue }
179
+ }
180
+ }
181
+
182
+ // Usage in view
183
+ struct LoginView: View {
184
+ @Environment(\.authService) private var authService // ✅ Injected dependency
185
+
186
+ var body: some View {
187
+ Button("Login") {
188
+ Task {
189
+ await authService.login(email: email, password: password)
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // Provide in app
196
+ @main
197
+ struct MyApp: App {
198
+ var body: some Scene {
199
+ WindowGroup {
200
+ ContentView()
201
+ .environment(\.authService, productionAuthService) // ✅ Provide
202
+ }
203
+ }
204
+ }
205
+ ```
206
+
207
+ ✅ **Good: Built-in environment values**
208
+ ```swift
209
+ struct ContentView: View {
210
+ @Environment(\.dismiss) private var dismiss // ✅ Dismissal
211
+ @Environment(\.colorScheme) private var colorScheme // ✅ Color scheme
212
+ @Environment(\.horizontalSizeClass) private var sizeClass // ✅ Size class
213
+
214
+ var body: some View {
215
+ Button("Close") {
216
+ dismiss() // ✅ Use environment value
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### 1.5 State Ownership Rules
223
+
224
+ **Check for:**
225
+ - [ ] Single source of truth
226
+ - [ ] Clear state ownership
227
+ - [ ] No duplicate state
228
+ - [ ] Derived state computed, not stored
229
+
230
+ **Examples:**
231
+
232
+ ❌ **Bad: Duplicate state**
233
+ ```swift
234
+ @Observable
235
+ final class UserViewModel {
236
+ var users: [User] = []
237
+ var userCount: Int = 0 // ❌ Duplicate - derived from users
238
+
239
+ func addUser(_ user: User) {
240
+ users.append(user)
241
+ userCount = users.count // ❌ Manual sync
242
+ }
243
+ }
244
+ ```
245
+
246
+ ✅ **Good: Computed property**
247
+ ```swift
248
+ @Observable
249
+ final class UserViewModel {
250
+ var users: [User] = []
251
+
252
+ var userCount: Int { // ✅ Computed from users
253
+ users.count
254
+ }
255
+
256
+ func addUser(_ user: User) {
257
+ users.append(user) // ✅ Single source of truth
258
+ }
259
+ }
260
+ ```
261
+
262
+ ---
263
+
264
+ ## 2. Property Wrapper Selection
265
+
266
+ ### 2.1 Property Wrapper Decision Tree
267
+
268
+ **Use this decision tree:**
269
+
270
+ ```
271
+ Is this UI-related mutable state?
272
+ ├─ Yes → Is it owned by this view?
273
+ │ ├─ Yes → Use @State
274
+ │ └─ No → Is it a two-way binding from parent?
275
+ │ ├─ Yes → Use @Binding
276
+ │ └─ No → Is it an observable object?
277
+ │ ├─ Yes (iOS 17+) → Use @Observable class (no wrapper in view)
278
+ │ └─ Yes (iOS 16-) → Use @StateObject or @ObservedObject
279
+ ├─ No → Is it environment data?
280
+ ├─ Yes → Use @Environment
281
+ └─ No → Use let (immutable property)
282
+ ```
283
+
284
+ ### 2.2 Property Wrapper Reference Table
285
+
286
+ | Wrapper | iOS Version | Use Case | Example |
287
+ |---------|-------------|----------|---------|
288
+ | `@State` | iOS 13+ | View-local mutable state | `@State private var isExpanded = false` |
289
+ | `@Binding` | iOS 13+ | Two-way binding from parent | `@Binding var text: String` |
290
+ | `@Observable` | iOS 17+ | Observable view model (class) | `@Observable final class ViewModel { }` |
291
+ | `@StateObject` | iOS 14+ | View owns observable object (legacy) | `@StateObject private var vm = VM()` |
292
+ | `@ObservedObject` | iOS 13+ | Parent owns observable object (legacy) | `@ObservedObject var vm: VM` |
293
+ | `@Environment` | iOS 13+ | Environment dependency injection | `@Environment(\.dismiss) var dismiss` |
294
+ | `@EnvironmentObject` | iOS 13+ | Shared observable across views | `@EnvironmentObject var settings: Settings` |
295
+ | `@AppStorage` | iOS 14+ | UserDefaults-backed property | `@AppStorage("theme") var theme = "light"` |
296
+ | `@SceneStorage` | iOS 14+ | Scene-specific state restoration | `@SceneStorage("selectedTab") var tab = 0` |
297
+ | `@FocusState` | iOS 15+ | Focus state for text fields | `@FocusState private var isFocused: Bool` |
298
+
299
+ ### 2.3 Common Mistakes
300
+
301
+ **Check for:**
302
+ - [ ] No @StateObject with @Observable classes
303
+ - [ ] No @Published with @Observable classes
304
+ - [ ] No @State for objects (use @Observable instead)
305
+ - [ ] No @Binding for read-only data
306
+
307
+ **Examples:**
308
+
309
+ ❌ **Bad: @StateObject with @Observable**
310
+ ```swift
311
+ @Observable
312
+ final class ViewModel { }
313
+
314
+ struct MyView: View {
315
+ @StateObject private var viewModel = ViewModel() // ❌ Don't mix
316
+ }
317
+ ```
318
+
319
+ ✅ **Good: No wrapper with @Observable**
320
+ ```swift
321
+ @Observable
322
+ final class ViewModel { }
323
+
324
+ struct MyView: View {
325
+ let viewModel = ViewModel() // ✅ No wrapper needed (iOS 17+)
326
+ }
327
+ ```
328
+
329
+ ❌ **Bad: @Published with @Observable**
330
+ ```swift
331
+ @Observable
332
+ final class ViewModel {
333
+ @Published var text: String = "" // ❌ Don't mix
334
+ }
335
+ ```
336
+
337
+ ✅ **Good: Regular property with @Observable**
338
+ ```swift
339
+ @Observable
340
+ final class ViewModel {
341
+ var text: String = "" // ✅ Automatically observable
342
+ }
343
+ ```
344
+
345
+ ---
346
+
347
+ ## 3. Modern API Usage
348
+
349
+ ### 3.1 NavigationStack vs NavigationView
350
+
351
+ **Check for:**
352
+ - [ ] NavigationStack used instead of NavigationView (iOS 16+)
353
+ - [ ] Proper navigation path management
354
+ - [ ] Type-safe navigation destinations
355
+
356
+ **Examples:**
357
+
358
+ ❌ **Bad: Deprecated NavigationView**
359
+ ```swift
360
+ NavigationView { // ❌ Deprecated in iOS 16
361
+ List(items) { item in
362
+ NavigationLink(destination: DetailView(item: item)) {
363
+ Text(item.name)
364
+ }
365
+ }
366
+ }
367
+ ```
368
+
369
+ ✅ **Good: NavigationStack**
370
+ ```swift
371
+ NavigationStack { // ✅ Modern (iOS 16+)
372
+ List(items) { item in
373
+ NavigationLink(value: item) { // ✅ Value-based
374
+ Text(item.name)
375
+ }
376
+ }
377
+ .navigationDestination(for: Item.self) { item in
378
+ DetailView(item: item)
379
+ }
380
+ }
381
+ ```
382
+
383
+ ✅ **Good: NavigationStack with path**
384
+ ```swift
385
+ @Observable
386
+ final class NavigationModel {
387
+ var path = NavigationPath()
388
+
389
+ func navigateToDetail(_ item: Item) {
390
+ path.append(item)
391
+ }
392
+ }
393
+
394
+ struct ContentView: View {
395
+ @State private var navModel = NavigationModel()
396
+
397
+ var body: some View {
398
+ NavigationStack(path: $navModel.path) { // ✅ Programmatic navigation
399
+ // Content
400
+ }
401
+ }
402
+ }
403
+ ```
404
+
405
+ ### 3.2 .task vs .onAppear for Async Work
406
+
407
+ **Check for:**
408
+ - [ ] .task modifier for async work instead of .onAppear
409
+ - [ ] Automatic cancellation handling with .task
410
+ - [ ] No manual Task creation in .onAppear
411
+
412
+ **Examples:**
413
+
414
+ ❌ **Bad: .onAppear with manual Task**
415
+ ```swift
416
+ .onAppear {
417
+ Task { // ❌ Manual task, no automatic cancellation
418
+ await viewModel.load()
419
+ }
420
+ }
421
+ ```
422
+
423
+ ✅ **Good: .task modifier**
424
+ ```swift
425
+ .task { // ✅ Automatically cancelled when view disappears
426
+ await viewModel.load()
427
+ }
428
+ ```
429
+
430
+ ✅ **Good: .task with id for refresh**
431
+ ```swift
432
+ .task(id: selectedCategory) { // ✅ Runs again when id changes
433
+ await viewModel.load(category: selectedCategory)
434
+ }
435
+ ```
436
+
437
+ ### 3.3 .onChange with Modern Syntax (iOS 17+)
438
+
439
+ **Check for:**
440
+ - [ ] Modern .onChange syntax (iOS 17+)
441
+ - [ ] Access to both old and new values
442
+ - [ ] No deprecated .onChange(of:perform:)
443
+
444
+ **Examples:**
445
+
446
+ ❌ **Bad: Old .onChange syntax**
447
+ ```swift
448
+ .onChange(of: searchText) { newValue in // ❌ Old syntax
449
+ performSearch(newValue)
450
+ }
451
+ ```
452
+
453
+ ✅ **Good: Modern .onChange syntax**
454
+ ```swift
455
+ .onChange(of: searchText) { oldValue, newValue in // ✅ New syntax (iOS 17+)
456
+ performSearch(newValue)
457
+ }
458
+ ```
459
+
460
+ ✅ **Good: Modern .onChange with initial value**
461
+ ```swift
462
+ .onChange(of: searchText, initial: true) { oldValue, newValue in // ✅ Runs on appear
463
+ performSearch(newValue)
464
+ }
465
+ ```
466
+
467
+ ### 3.4 Deprecated APIs to Replace
468
+
469
+ **Check for and replace:**
470
+
471
+ | Deprecated API | Modern Replacement | iOS Version |
472
+ |----------------|-------------------|-------------|
473
+ | `NavigationView` | `NavigationStack` | iOS 16+ |
474
+ | `.onAppear { Task { } }` | `.task { }` | iOS 15+ |
475
+ | `.onChange(of:perform:)` | `.onChange(of:) { old, new in }` | iOS 17+ |
476
+ | `@StateObject` with `ObservableObject` | `@Observable` class | iOS 17+ |
477
+ | `@Published` | Regular property with `@Observable` | iOS 17+ |
478
+ | `GeometryReader` (simple cases) | `.frame(maxWidth: .infinity)` | iOS 13+ |
479
+ | `List { ... }` with explicit ForEach | `List(items) { }` | iOS 13+ |
480
+
481
+ ---
482
+
483
+ ## 4. View Composition
484
+
485
+ ### 4.1 View Extraction Guidelines
486
+
487
+ **Check for:**
488
+ - [ ] View body < 50 lines (guideline)
489
+ - [ ] Logical subviews extracted
490
+ - [ ] Reusable components identified
491
+ - [ ] Proper view hierarchy depth (< 5 levels)
492
+
493
+ **Examples:**
494
+
495
+ ❌ **Bad: Monolithic view**
496
+ ```swift
497
+ struct LoginView: View {
498
+ var body: some View {
499
+ VStack(spacing: 20) {
500
+ Image("logo")
501
+ .resizable()
502
+ .frame(width: 100, height: 100)
503
+ Text("Welcome")
504
+ .font(.title)
505
+ TextField("Email", text: $email)
506
+ .textFieldStyle(.roundedBorder)
507
+ SecureField("Password", text: $password)
508
+ .textFieldStyle(.roundedBorder)
509
+ Button("Login") {
510
+ login()
511
+ }
512
+ .buttonStyle(.borderedProminent)
513
+ // ... 50 more lines
514
+ } // ❌ Too long, no extraction
515
+ }
516
+ }
517
+ ```
518
+
519
+ ✅ **Good: Extracted subviews**
520
+ ```swift
521
+ struct LoginView: View {
522
+ var body: some View {
523
+ VStack(spacing: 20) {
524
+ LoginHeaderView() // ✅ Extracted
525
+ LoginFormView(
526
+ email: $email,
527
+ password: $password
528
+ ) // ✅ Extracted
529
+ LoginActionsView(
530
+ onLogin: login
531
+ ) // ✅ Extracted
532
+ }
533
+ }
534
+ }
535
+
536
+ // MARK: - Subviews
537
+ private struct LoginHeaderView: View {
538
+ var body: some View {
539
+ VStack {
540
+ Image("logo")
541
+ .resizable()
542
+ .frame(width: 100, height: 100)
543
+ Text("Welcome")
544
+ .font(.title)
545
+ }
546
+ }
547
+ }
548
+
549
+ private struct LoginFormView: View {
550
+ @Binding var email: String
551
+ @Binding var password: String
552
+
553
+ var body: some View {
554
+ VStack(spacing: 12) {
555
+ TextField("Email", text: $email)
556
+ .textFieldStyle(.roundedBorder)
557
+ SecureField("Password", text: $password)
558
+ .textFieldStyle(.roundedBorder)
559
+ }
560
+ }
561
+ }
562
+ ```
563
+
564
+ ### 4.2 When to Extract
565
+
566
+ **Extract when:**
567
+ - View body > 50 lines
568
+ - Logic is reused in multiple places
569
+ - Clear semantic boundary (header, form, footer)
570
+ - Testing would benefit from isolation
571
+ - View hierarchy becomes too deep
572
+
573
+ **Don't extract when:**
574
+ - View is small and simple (< 20 lines)
575
+ - Only used once and tightly coupled
576
+ - Extraction adds unnecessary complexity
577
+
578
+ ### 4.3 ViewBuilder Patterns
579
+
580
+ **Check for:**
581
+ - [ ] @ViewBuilder for conditional view logic
582
+ - [ ] @ViewBuilder for custom container views
583
+ - [ ] Proper use of view builder syntax
584
+
585
+ **Examples:**
586
+
587
+ ✅ **Good: @ViewBuilder for conditional content**
588
+ ```swift
589
+ struct ConditionalView<Content: View>: View {
590
+ let showHeader: Bool
591
+ @ViewBuilder let content: () -> Content
592
+
593
+ var body: some View {
594
+ VStack {
595
+ if showHeader {
596
+ HeaderView()
597
+ }
598
+ content()
599
+ }
600
+ }
601
+ }
602
+
603
+ // Usage
604
+ ConditionalView(showHeader: true) {
605
+ Text("Content")
606
+ Button("Action") { }
607
+ }
608
+ ```
609
+
610
+ ✅ **Good: @ViewBuilder for custom container**
611
+ ```swift
612
+ struct Card<Content: View>: View {
613
+ @ViewBuilder let content: () -> Content
614
+
615
+ var body: some View {
616
+ VStack(alignment: .leading, spacing: 8) {
617
+ content()
618
+ }
619
+ .padding()
620
+ .background(Color.white)
621
+ .cornerRadius(8)
622
+ .shadow(radius: 2)
623
+ }
624
+ }
625
+
626
+ // Usage
627
+ Card {
628
+ Text("Title")
629
+ Text("Subtitle")
630
+ Button("Action") { }
631
+ }
632
+ ```
633
+
634
+ ---
635
+
636
+ ## 5. Accessibility
637
+
638
+ ### 5.1 Accessibility Labels
639
+
640
+ **Check for:**
641
+ - [ ] Accessibility labels for non-text elements
642
+ - [ ] Descriptive labels (not just button text)
643
+ - [ ] Labels for images and icons
644
+
645
+ **Examples:**
646
+
647
+ ❌ **Bad: No accessibility labels**
648
+ ```swift
649
+ Image(systemName: "trash") // ❌ No label
650
+ .onTapGesture {
651
+ deleteItem()
652
+ }
653
+ ```
654
+
655
+ ✅ **Good: Accessibility labels**
656
+ ```swift
657
+ Image(systemName: "trash")
658
+ .onTapGesture {
659
+ deleteItem()
660
+ }
661
+ .accessibilityLabel("Delete item") // ✅ Clear label
662
+ ```
663
+
664
+ ✅ **Good: Accessibility for complex views**
665
+ ```swift
666
+ HStack {
667
+ Image(systemName: "star.fill")
668
+ Text("\(rating)")
669
+ }
670
+ .accessibilityElement(children: .combine) // ✅ Combine children
671
+ .accessibilityLabel("Rating: \(rating) stars") // ✅ Clear description
672
+ ```
673
+
674
+ ### 5.2 Accessibility Hints
675
+
676
+ **Check for:**
677
+ - [ ] Hints for non-obvious interactions
678
+ - [ ] Clear, concise hints
679
+ - [ ] No redundant hints
680
+
681
+ **Examples:**
682
+
683
+ ✅ **Good: Accessibility hints**
684
+ ```swift
685
+ Button("Share") {
686
+ shareContent()
687
+ }
688
+ .accessibilityLabel("Share")
689
+ .accessibilityHint("Opens the share sheet") // ✅ Describes action
690
+ ```
691
+
692
+ ### 5.3 Accessibility Traits
693
+
694
+ **Check for:**
695
+ - [ ] Appropriate traits for elements
696
+ - [ ] Button trait for tappable elements
697
+ - [ ] Header trait for section headers
698
+
699
+ **Examples:**
700
+
701
+ ✅ **Good: Accessibility traits**
702
+ ```swift
703
+ Text("Settings")
704
+ .font(.title)
705
+ .accessibilityAddTraits(.isHeader) // ✅ Mark as header
706
+
707
+ Image(systemName: "gear")
708
+ .onTapGesture {
709
+ openSettings()
710
+ }
711
+ .accessibilityAddTraits(.isButton) // ✅ Mark as button
712
+ .accessibilityLabel("Settings")
713
+ ```
714
+
715
+ ### 5.4 Dynamic Type Support
716
+
717
+ **Check for:**
718
+ - [ ] System fonts used (automatically scale)
719
+ - [ ] Custom fonts with .dynamicTypeSize
720
+ - [ ] Layout adapts to large text sizes
721
+
722
+ **Examples:**
723
+
724
+ ✅ **Good: System fonts (automatic scaling)**
725
+ ```swift
726
+ Text("Title")
727
+ .font(.title) // ✅ Automatically scales
728
+
729
+ Text("Body")
730
+ .font(.body) // ✅ Automatically scales
731
+ ```
732
+
733
+ ✅ **Good: Custom font with scaling**
734
+ ```swift
735
+ Text("Custom")
736
+ .font(.custom("CustomFont", size: 16, relativeTo: .body)) // ✅ Scales
737
+ ```
738
+
739
+ ✅ **Good: Layout adaptation**
740
+ ```swift
741
+ @Environment(\.dynamicTypeSize) private var dynamicTypeSize
742
+
743
+ var body: some View {
744
+ if dynamicTypeSize.isAccessibilitySize {
745
+ VStack { // ✅ Vertical for large text
746
+ labelView
747
+ valueView
748
+ }
749
+ } else {
750
+ HStack { // ✅ Horizontal for normal text
751
+ labelView
752
+ valueView
753
+ }
754
+ }
755
+ }
756
+ ```
757
+
758
+ ---
759
+
760
+ ## 6. Performance Patterns
761
+
762
+ ### 6.1 Equatable Conformance
763
+
764
+ **Check for:**
765
+ - [ ] View models conform to Equatable
766
+ - [ ] Proper equality implementation
767
+ - [ ] Reduced view updates
768
+
769
+ **Examples:**
770
+
771
+ ✅ **Good: Equatable view model**
772
+ ```swift
773
+ @Observable
774
+ final class UserViewModel: Equatable { // ✅ Equatable
775
+ let id: UUID
776
+ var name: String
777
+ var email: String
778
+
779
+ static func == (lhs: UserViewModel, rhs: UserViewModel) -> Bool {
780
+ lhs.id == rhs.id &&
781
+ lhs.name == rhs.name &&
782
+ lhs.email == rhs.email
783
+ }
784
+ }
785
+
786
+ struct UserRow: View {
787
+ let viewModel: UserViewModel
788
+
789
+ var body: some View {
790
+ HStack {
791
+ Text(viewModel.name)
792
+ Text(viewModel.email)
793
+ }
794
+ }
795
+ .equatable() // ✅ Only updates when viewModel changes
796
+ }
797
+ ```
798
+
799
+ ### 6.2 Avoid Heavy Work in Body
800
+
801
+ **Check for:**
802
+ - [ ] No computation in body property
803
+ - [ ] Computed properties for derived values
804
+ - [ ] View model handles complex logic
805
+
806
+ **Examples:**
807
+
808
+ ❌ **Bad: Computation in body**
809
+ ```swift
810
+ var body: some View {
811
+ let sortedItems = items.sorted { $0.date > $1.date } // ❌ Every render
812
+ List(sortedItems) { item in
813
+ ItemRow(item: item)
814
+ }
815
+ }
816
+ ```
817
+
818
+ ✅ **Good: Computed property or view model**
819
+ ```swift
820
+ @Observable
821
+ final class ItemListViewModel {
822
+ var items: [Item] = []
823
+
824
+ var sortedItems: [Item] { // ✅ Computed property
825
+ items.sorted { $0.date > $1.date }
826
+ }
827
+ }
828
+
829
+ var body: some View {
830
+ List(viewModel.sortedItems) { item in // ✅ Uses cached result
831
+ ItemRow(item: item)
832
+ }
833
+ }
834
+ ```
835
+
836
+ ---
837
+
838
+ ## 7. Preview Configurations
839
+
840
+ ### 7.1 Preview Macros (iOS 17+)
841
+
842
+ **Check for:**
843
+ - [ ] #Preview macro used instead of PreviewProvider
844
+ - [ ] Multiple preview configurations
845
+ - [ ] Sample data for previews
846
+
847
+ **Examples:**
848
+
849
+ ❌ **Bad: Old PreviewProvider**
850
+ ```swift
851
+ struct LoginView_Previews: PreviewProvider { // ❌ Old pattern
852
+ static var previews: some View {
853
+ LoginView()
854
+ }
855
+ }
856
+ ```
857
+
858
+ ✅ **Good: Modern #Preview macro**
859
+ ```swift
860
+ #Preview { // ✅ Modern preview (iOS 17+)
861
+ LoginView()
862
+ }
863
+
864
+ #Preview("Dark Mode") { // ✅ Named preview
865
+ LoginView()
866
+ .preferredColorScheme(.dark)
867
+ }
868
+
869
+ #Preview("Large Text") { // ✅ Accessibility preview
870
+ LoginView()
871
+ .environment(\.dynamicTypeSize, .xxxLarge)
872
+ }
873
+ ```
874
+
875
+ ---
876
+
877
+ ## Quick Reference Checklist
878
+
879
+ ### Critical Issues
880
+ - [ ] No @StateObject with @Observable classes (iOS 17+)
881
+ - [ ] No @Published with @Observable classes
882
+ - [ ] No heavy computation in view body
883
+ - [ ] Proper state ownership (single source of truth)
884
+
885
+ ### High Priority
886
+ - [ ] @Observable used for view models (iOS 17+)
887
+ - [ ] NavigationStack instead of NavigationView (iOS 16+)
888
+ - [ ] .task instead of .onAppear for async work (iOS 15+)
889
+ - [ ] Proper property wrapper selection
890
+ - [ ] View extraction for complex views
891
+
892
+ ### Medium Priority
893
+ - [ ] Modern .onChange syntax (iOS 17+)
894
+ - [ ] Accessibility labels and hints
895
+ - [ ] Dynamic Type support
896
+ - [ ] Equatable conformance for view models
897
+ - [ ] #Preview macro (iOS 17+)
898
+
899
+ ### Low Priority
900
+ - [ ] View body < 50 lines
901
+ - [ ] MARK comments for subviews
902
+ - [ ] Preview configurations for testing
903
+
904
+ ---
905
+
906
+ ## Version
907
+ **Last Updated**: 2026-02-10
908
+ **Version**: 1.0.0
909
+ **iOS Version**: iOS 17+, macOS 14+, watchOS 10+, tvOS 17+, visionOS 1+