swift-code-reviewer-skill 1.0.0 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/README.md +76 -444
- package/SKILL.md +97 -7
- package/package.json +1 -1
- package/references/architecture-patterns.md +275 -0
- package/references/performance-review.md +193 -0
- package/references/review-workflow.md +121 -0
- package/references/swiftui-review-checklist.md +738 -0
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: swift-code-reviewer
|
|
3
|
-
description: Perform thorough code reviews for Swift/SwiftUI code, analyzing code quality, architecture, performance, security, and adherence to Swift 6+ best practices, SwiftUI patterns, iOS/macOS platform guidelines, and project-specific coding standards from .claude/CLAUDE.md. Use when reviewing code changes, performing quality audits, or providing structured feedback on Swift codebases with all severity levels and positive feedback.
|
|
3
|
+
description: Perform thorough code reviews for Swift/SwiftUI code, analyzing code quality, architecture, performance, security, and adherence to Swift 6+ best practices, SwiftUI patterns, navigation architecture, sheet routing, theming, async state, iOS/macOS platform guidelines, and project-specific coding standards from .claude/CLAUDE.md. Use when reviewing code changes, performing quality audits, or providing structured feedback on Swift codebases with all severity levels and positive feedback.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Swift/SwiftUI Code Review Skill
|
|
@@ -30,6 +30,7 @@ Use this skill when you need to:
|
|
|
30
30
|
- Provide structured feedback to team members
|
|
31
31
|
|
|
32
32
|
**Trigger patterns:**
|
|
33
|
+
|
|
33
34
|
- "Review this PR"
|
|
34
35
|
- "Review [filename].swift"
|
|
35
36
|
- "Review my changes"
|
|
@@ -69,6 +70,7 @@ The skill follows a **four-phase workflow** to ensure comprehensive and actionab
|
|
|
69
70
|
Execute checks across **six core categories**:
|
|
70
71
|
|
|
71
72
|
#### 1. Swift Best Practices
|
|
73
|
+
|
|
72
74
|
Reference: `swift-best-practices` skill knowledge base
|
|
73
75
|
|
|
74
76
|
- **Concurrency Safety**
|
|
@@ -91,6 +93,7 @@ Reference: `swift-best-practices` skill knowledge base
|
|
|
91
93
|
- Proper use of optionals
|
|
92
94
|
|
|
93
95
|
#### 2. SwiftUI Quality
|
|
96
|
+
|
|
94
97
|
Reference: `swiftui-expert-skill` knowledge base
|
|
95
98
|
|
|
96
99
|
- **State Management**
|
|
@@ -118,6 +121,7 @@ Reference: `swiftui-expert-skill` knowledge base
|
|
|
118
121
|
- Keyboard navigation
|
|
119
122
|
|
|
120
123
|
#### 3. Performance
|
|
124
|
+
|
|
121
125
|
Reference: `swiftui-performance-audit` knowledge base
|
|
122
126
|
|
|
123
127
|
- **View Optimization**
|
|
@@ -256,7 +260,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
256
260
|
- Comparison with project guidelines
|
|
257
261
|
|
|
258
262
|
3. **Provide Context**
|
|
259
|
-
- Explain
|
|
263
|
+
- Explain _why_ something is an issue
|
|
260
264
|
- Reference best practices or standards
|
|
261
265
|
- Suggest specific fixes with examples
|
|
262
266
|
- Link to learning resources
|
|
@@ -266,6 +270,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
266
270
|
### 1. Swift Language Quality
|
|
267
271
|
|
|
268
272
|
**What to Check:**
|
|
273
|
+
|
|
269
274
|
- Concurrency patterns (actors, async/await, Sendable)
|
|
270
275
|
- Error handling (typed throws, Result type)
|
|
271
276
|
- Optionals handling (avoid force unwrapping)
|
|
@@ -279,6 +284,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
279
284
|
### 2. SwiftUI Patterns
|
|
280
285
|
|
|
281
286
|
**What to Check:**
|
|
287
|
+
|
|
282
288
|
- Property wrapper selection and usage
|
|
283
289
|
- State management patterns
|
|
284
290
|
- View lifecycle understanding
|
|
@@ -292,6 +298,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
292
298
|
### 3. Performance Optimization
|
|
293
299
|
|
|
294
300
|
**What to Check:**
|
|
301
|
+
|
|
295
302
|
- View update optimization
|
|
296
303
|
- ForEach identity and performance
|
|
297
304
|
- Heavy work in view body
|
|
@@ -305,6 +312,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
305
312
|
### 4. Security & Safety
|
|
306
313
|
|
|
307
314
|
**What to Check:**
|
|
315
|
+
|
|
308
316
|
- Force unwrap detection (`!`, `as!`, `try!`)
|
|
309
317
|
- Input validation and sanitization
|
|
310
318
|
- Sensitive data handling (passwords, tokens)
|
|
@@ -318,6 +326,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
318
326
|
### 5. Architecture & Design
|
|
319
327
|
|
|
320
328
|
**What to Check:**
|
|
329
|
+
|
|
321
330
|
- MVVM, MVI, TCA, or other architecture compliance
|
|
322
331
|
- Dependency injection patterns
|
|
323
332
|
- Separation of concerns
|
|
@@ -331,6 +340,7 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
331
340
|
### 6. Project-Specific Standards
|
|
332
341
|
|
|
333
342
|
**What to Check:**
|
|
343
|
+
|
|
334
344
|
- `.claude/CLAUDE.md` compliance
|
|
335
345
|
- Custom architecture patterns
|
|
336
346
|
- Design system usage
|
|
@@ -345,9 +355,11 @@ Reference: `swiftui-performance-audit` knowledge base
|
|
|
345
355
|
This skill **references** (not duplicates) three foundational skills for domain expertise:
|
|
346
356
|
|
|
347
357
|
### 1. swift-best-practices
|
|
358
|
+
|
|
348
359
|
**When to Use:** Reviewing Swift language usage, concurrency patterns, API design, or Swift 6+ migration
|
|
349
360
|
|
|
350
361
|
**What it Provides:**
|
|
362
|
+
|
|
351
363
|
- Swift 6+ concurrency patterns (actors, async/await, Sendable)
|
|
352
364
|
- API design guidelines compliance
|
|
353
365
|
- Availability pattern validation
|
|
@@ -355,14 +367,17 @@ This skill **references** (not duplicates) three foundational skills for domain
|
|
|
355
367
|
- Modern Swift feature adoption
|
|
356
368
|
|
|
357
369
|
**How to Leverage:**
|
|
370
|
+
|
|
358
371
|
- Read `~/.claude/skills/swift-best-practices/references/concurrency.md` for concurrency checks
|
|
359
372
|
- Reference `swift6-features.md` for Swift 6 migration patterns
|
|
360
373
|
- Use `api-design.md` for naming and parameter validation
|
|
361
374
|
|
|
362
375
|
### 2. swiftui-expert-skill
|
|
376
|
+
|
|
363
377
|
**When to Use:** Reviewing SwiftUI views, state management, or UI code
|
|
364
378
|
|
|
365
379
|
**What it Provides:**
|
|
380
|
+
|
|
366
381
|
- State management patterns (@Observable, @State, @Binding)
|
|
367
382
|
- Modern SwiftUI API guidance (iOS 17+, macOS 14+)
|
|
368
383
|
- View composition best practices
|
|
@@ -370,14 +385,17 @@ This skill **references** (not duplicates) three foundational skills for domain
|
|
|
370
385
|
- Accessibility patterns
|
|
371
386
|
|
|
372
387
|
**How to Leverage:**
|
|
388
|
+
|
|
373
389
|
- Read `~/.claude/skills/swiftui-expert-skill/references/state-management.md` for property wrapper checks
|
|
374
390
|
- Reference `modern-apis.md` for deprecation detection
|
|
375
391
|
- Use `view-composition.md` for component structure validation
|
|
376
392
|
|
|
377
393
|
### 3. swiftui-performance-audit
|
|
394
|
+
|
|
378
395
|
**When to Use:** Performance concerns identified or mentioned in PR description
|
|
379
396
|
|
|
380
397
|
**What it Provides:**
|
|
398
|
+
|
|
381
399
|
- View update optimization patterns
|
|
382
400
|
- ForEach performance analysis
|
|
383
401
|
- Layout thrash detection
|
|
@@ -385,11 +403,33 @@ This skill **references** (not duplicates) three foundational skills for domain
|
|
|
385
403
|
- Memory management
|
|
386
404
|
|
|
387
405
|
**How to Leverage:**
|
|
406
|
+
|
|
388
407
|
- Read `~/.claude/skills/swiftui-performance-audit/SKILL.md` for performance audit workflow
|
|
389
408
|
- Reference performance-specific checks when reviewing view code
|
|
390
409
|
- Apply recommendations from the skill to performance-sensitive paths
|
|
391
410
|
|
|
411
|
+
### 4. swiftui-ui-patterns
|
|
412
|
+
|
|
413
|
+
**When to Use:** Reviewing navigation architecture, sheet/modal routing, TabView setup, theming, async state management, focus handling, or API client patterns
|
|
414
|
+
|
|
415
|
+
**What it Provides:**
|
|
416
|
+
|
|
417
|
+
- Navigation architecture (route enums, RouterPath, centralized navigationDestination)
|
|
418
|
+
- Sheet/modal routing (item-driven sheets, SheetDestination enum)
|
|
419
|
+
- TabView with independent per-tab navigation history
|
|
420
|
+
- Theming with semantic colors via `@Environment(Theme.self)`
|
|
421
|
+
- Async state patterns (`.task(id:)`, LoadState enum, CancellationError handling)
|
|
422
|
+
- Focus chaining with FocusField enum and `.onSubmit`
|
|
423
|
+
- Lightweight API client pattern (closure-based structs, `.live()` / `.mock()` factories)
|
|
424
|
+
|
|
425
|
+
**How to Leverage:**
|
|
426
|
+
|
|
427
|
+
- Read `~/.claude/skills/swiftui-ui-patterns/references/navigation.md` for route enum and RouterPath checks
|
|
428
|
+
- Reference `sheets-modals.md` for sheet routing validation
|
|
429
|
+
- Use `theming.md` for semantic color enforcement
|
|
430
|
+
|
|
392
431
|
**Integration Strategy:**
|
|
432
|
+
|
|
393
433
|
1. Load relevant reference files from these skills as needed
|
|
394
434
|
2. Apply their checklist items to the review
|
|
395
435
|
3. Reference their documentation in feedback
|
|
@@ -398,6 +438,7 @@ This skill **references** (not duplicates) three foundational skills for domain
|
|
|
398
438
|
## Platform Support
|
|
399
439
|
|
|
400
440
|
### GitHub Pull Requests
|
|
441
|
+
|
|
401
442
|
Use `gh` CLI for fetching PR data:
|
|
402
443
|
|
|
403
444
|
```bash
|
|
@@ -415,6 +456,7 @@ gh pr view <PR-number> --json comments
|
|
|
415
456
|
```
|
|
416
457
|
|
|
417
458
|
### GitLab Merge Requests
|
|
459
|
+
|
|
418
460
|
Use `glab` CLI for fetching MR data:
|
|
419
461
|
|
|
420
462
|
```bash
|
|
@@ -432,6 +474,7 @@ glab mr note list <MR-number>
|
|
|
432
474
|
```
|
|
433
475
|
|
|
434
476
|
### Local Git Changes
|
|
477
|
+
|
|
435
478
|
For uncommitted changes or manual review:
|
|
436
479
|
|
|
437
480
|
```bash
|
|
@@ -452,10 +495,11 @@ git show <commit-hash>
|
|
|
452
495
|
|
|
453
496
|
The review report follows this structure:
|
|
454
497
|
|
|
455
|
-
|
|
498
|
+
````markdown
|
|
456
499
|
# Code Review Report
|
|
457
500
|
|
|
458
501
|
## Summary
|
|
502
|
+
|
|
459
503
|
- **Files Reviewed**: X
|
|
460
504
|
- **Total Findings**: Y
|
|
461
505
|
- **Critical**: 0
|
|
@@ -466,6 +510,7 @@ The review report follows this structure:
|
|
|
466
510
|
- **Refactoring Suggestions**: 4
|
|
467
511
|
|
|
468
512
|
## Executive Summary
|
|
513
|
+
|
|
469
514
|
[Brief overview of the changes and overall code quality]
|
|
470
515
|
|
|
471
516
|
---
|
|
@@ -475,6 +520,7 @@ The review report follows this structure:
|
|
|
475
520
|
### File: Sources/Features/Login/LoginView.swift
|
|
476
521
|
|
|
477
522
|
#### ✅ Positive Feedback
|
|
523
|
+
|
|
478
524
|
1. **Excellent State Management** (line 23)
|
|
479
525
|
- Proper use of @Observable for view model
|
|
480
526
|
- Clean separation of concerns
|
|
@@ -484,11 +530,13 @@ The review report follows this structure:
|
|
|
484
530
|
- Proper async/await integration
|
|
485
531
|
|
|
486
532
|
#### 🔴 Critical Issues
|
|
533
|
+
|
|
487
534
|
1. **Data Race Risk** (line 67)
|
|
488
535
|
- **Severity**: Critical
|
|
489
536
|
- **Category**: Concurrency
|
|
490
537
|
- **Issue**: Mutable state accessed from multiple actors without synchronization
|
|
491
538
|
- **Fix**:
|
|
539
|
+
|
|
492
540
|
```swift
|
|
493
541
|
// Before
|
|
494
542
|
class LoginViewModel {
|
|
@@ -501,14 +549,17 @@ The review report follows this structure:
|
|
|
501
549
|
@Published var isLoading = false
|
|
502
550
|
}
|
|
503
551
|
```
|
|
552
|
+
|
|
504
553
|
- **Reference**: swift-best-practices/references/concurrency.md
|
|
505
554
|
|
|
506
555
|
#### 🟡 High Priority
|
|
556
|
+
|
|
507
557
|
1. **Force Unwrap Detected** (line 89)
|
|
508
558
|
- **Severity**: High
|
|
509
559
|
- **Category**: Safety
|
|
510
560
|
- **Issue**: Force unwrapping optional can cause crash
|
|
511
561
|
- **Fix**:
|
|
562
|
+
|
|
512
563
|
```swift
|
|
513
564
|
// Before
|
|
514
565
|
let user = fetchUser()!
|
|
@@ -519,9 +570,11 @@ The review report follows this structure:
|
|
|
519
570
|
return
|
|
520
571
|
}
|
|
521
572
|
```
|
|
573
|
+
|
|
522
574
|
- **Reference**: Project coding standard (.claude/CLAUDE.md:45)
|
|
523
575
|
|
|
524
576
|
#### 💡 Refactoring Suggestions
|
|
577
|
+
|
|
525
578
|
1. **Extract Subview** (lines 120-150)
|
|
526
579
|
- Consider extracting login form into separate view
|
|
527
580
|
- Improves testability and reusability
|
|
@@ -531,20 +584,24 @@ The review report follows this structure:
|
|
|
531
584
|
## Prioritized Action Items
|
|
532
585
|
|
|
533
586
|
### Must Fix (Critical/High)
|
|
587
|
+
|
|
534
588
|
1. [ ] Fix data race in LoginViewModel.swift:67
|
|
535
589
|
2. [ ] Remove force unwrap in LoginView.swift:89
|
|
536
590
|
|
|
537
591
|
### Should Fix (Medium)
|
|
592
|
+
|
|
538
593
|
1. [ ] Add documentation to public APIs
|
|
539
594
|
2. [ ] Improve error handling in NetworkService.swift
|
|
540
595
|
|
|
541
596
|
### Consider (Low)
|
|
597
|
+
|
|
542
598
|
1. [ ] Refactor login form into separate view
|
|
543
599
|
2. [ ] Add more unit tests for edge cases
|
|
544
600
|
|
|
545
601
|
---
|
|
546
602
|
|
|
547
603
|
## Positive Patterns Observed
|
|
604
|
+
|
|
548
605
|
- Excellent use of @Observable for state management
|
|
549
606
|
- Consistent adherence to project architecture (MVVM)
|
|
550
607
|
- Comprehensive accessibility support
|
|
@@ -552,14 +609,16 @@ The review report follows this structure:
|
|
|
552
609
|
- Good test coverage for core functionality
|
|
553
610
|
|
|
554
611
|
## References
|
|
612
|
+
|
|
555
613
|
- [Swift Best Practices](~/.claude/skills/swift-best-practices/SKILL.md)
|
|
556
614
|
- [SwiftUI Expert Guide](~/.claude/skills/swiftui-expert-skill/SKILL.md)
|
|
557
615
|
- [Project Coding Standards](.claude/CLAUDE.md)
|
|
558
|
-
|
|
616
|
+
````
|
|
559
617
|
|
|
560
618
|
## How to Use
|
|
561
619
|
|
|
562
620
|
### Example 1: Review Specific File
|
|
621
|
+
|
|
563
622
|
```
|
|
564
623
|
User: "Review UserProfileView.swift"
|
|
565
624
|
|
|
@@ -571,6 +630,7 @@ Steps:
|
|
|
571
630
|
```
|
|
572
631
|
|
|
573
632
|
### Example 2: Review Git Changes
|
|
633
|
+
|
|
574
634
|
```
|
|
575
635
|
User: "Review my uncommitted changes"
|
|
576
636
|
|
|
@@ -583,6 +643,7 @@ Steps:
|
|
|
583
643
|
```
|
|
584
644
|
|
|
585
645
|
### Example 3: Review Pull Request
|
|
646
|
+
|
|
586
647
|
```
|
|
587
648
|
User: "Review PR #123"
|
|
588
649
|
|
|
@@ -596,6 +657,7 @@ Steps:
|
|
|
596
657
|
```
|
|
597
658
|
|
|
598
659
|
### Example 4: Review Against Custom Guidelines
|
|
660
|
+
|
|
599
661
|
```
|
|
600
662
|
User: "Review LoginViewModel.swift against our coding standards"
|
|
601
663
|
|
|
@@ -608,6 +670,7 @@ Steps:
|
|
|
608
670
|
```
|
|
609
671
|
|
|
610
672
|
### Example 5: Review Multiple Files
|
|
673
|
+
|
|
611
674
|
```
|
|
612
675
|
User: "Review all ViewModels in the Features folder"
|
|
613
676
|
|
|
@@ -619,21 +682,48 @@ Steps:
|
|
|
619
682
|
5. Summarize common patterns and issues across all files
|
|
620
683
|
```
|
|
621
684
|
|
|
685
|
+
### Example 6: Review Navigation / Routing Code
|
|
686
|
+
|
|
687
|
+
```
|
|
688
|
+
User: "Review our navigation setup and routing code"
|
|
689
|
+
|
|
690
|
+
Steps:
|
|
691
|
+
1. Read .claude/CLAUDE.md for project navigation patterns
|
|
692
|
+
2. Read router/coordinator files (RouterPath, AppCoordinator, TabRouter)
|
|
693
|
+
3. Read root views that set up NavigationStack and TabView
|
|
694
|
+
4. Run navigation architecture checks:
|
|
695
|
+
- Route destinations use typed Hashable enum (not String/Int)
|
|
696
|
+
- RouterPath @Observable owns path (not ad-hoc @State)
|
|
697
|
+
- Single centralized .navigationDestination per stack
|
|
698
|
+
- .sheet(item:) preferred over .sheet(isPresented:) when model selected
|
|
699
|
+
- Multiple sheets use SheetDestination enum (not multiple booleans)
|
|
700
|
+
- Each tab has independent RouterPath (not shared)
|
|
701
|
+
- .onOpenURL at app root, not scattered in feature views
|
|
702
|
+
5. Run async state checks:
|
|
703
|
+
- .task(id:) for input-driven async work
|
|
704
|
+
- CancellationError silenced
|
|
705
|
+
- LoadState<T> enum instead of multiple booleans
|
|
706
|
+
6. Generate report with navigation-specific findings
|
|
707
|
+
```
|
|
708
|
+
|
|
622
709
|
## Resources
|
|
623
710
|
|
|
624
711
|
This skill includes the following reference materials:
|
|
625
712
|
|
|
626
713
|
### Core References
|
|
714
|
+
|
|
627
715
|
- **review-workflow.md**: Detailed step-by-step review process, git commands, and diff parsing strategies
|
|
628
716
|
- **feedback-templates.md**: Templates for positive/negative comments, severity classification guidelines
|
|
629
717
|
|
|
630
718
|
### Quality Checklists
|
|
719
|
+
|
|
631
720
|
- **swift-quality-checklist.md**: Swift 6+ concurrency, error handling, optionals, access control, naming
|
|
632
721
|
- **swiftui-review-checklist.md**: Property wrappers, state management, modern APIs, view composition
|
|
633
722
|
- **performance-review.md**: View updates, ForEach optimization, layout efficiency, resource management
|
|
634
723
|
- **security-checklist.md**: Input validation, sensitive data, keychain, network security
|
|
635
724
|
|
|
636
725
|
### Architecture & Customization
|
|
726
|
+
|
|
637
727
|
- **architecture-patterns.md**: MVVM, MVI, TCA patterns, dependency injection, testing strategies
|
|
638
728
|
- **custom-guidelines.md**: How to read and parse .claude/CLAUDE.md and project-specific standards
|
|
639
729
|
|
|
@@ -652,7 +742,7 @@ This skill includes the following reference materials:
|
|
|
652
742
|
3. **Be Specific and Actionable**
|
|
653
743
|
- Include exact file:line references
|
|
654
744
|
- Provide code examples for fixes
|
|
655
|
-
- Explain
|
|
745
|
+
- Explain _why_ something is an issue
|
|
656
746
|
- Link to relevant documentation
|
|
657
747
|
|
|
658
748
|
4. **Prioritize by Severity**
|
|
@@ -685,6 +775,6 @@ For runtime analysis, recommend using Instruments or other profiling tools.
|
|
|
685
775
|
|
|
686
776
|
## Version
|
|
687
777
|
|
|
688
|
-
**Version**: 1.
|
|
689
|
-
**Last Updated**: 2026-
|
|
778
|
+
**Version**: 1.1.1
|
|
779
|
+
**Last Updated**: 2026-03-16
|
|
690
780
|
**Compatible with**: Swift 6+, SwiftUI (iOS 17+, macOS 14+, watchOS 10+, tvOS 17+, visionOS 1+)
|
package/package.json
CHANGED
|
@@ -593,6 +593,281 @@ struct UserListView: View {
|
|
|
593
593
|
|
|
594
594
|
---
|
|
595
595
|
|
|
596
|
+
## 5A. Lightweight Client Pattern (Closure-Based)
|
|
597
|
+
|
|
598
|
+
### 5A.1 Overview
|
|
599
|
+
|
|
600
|
+
**Purpose**: Define API clients as value-type structs with async closure properties, enabling easy swapping between live and mock implementations — especially for SwiftUI previews.
|
|
601
|
+
|
|
602
|
+
**Benefits:**
|
|
603
|
+
- Preview-friendly (no network calls in Xcode Previews)
|
|
604
|
+
- Testable without subclassing or protocol mocking boilerplate
|
|
605
|
+
- Composable: clients can be scoped per-feature
|
|
606
|
+
- No shared mutable singleton state
|
|
607
|
+
|
|
608
|
+
### 5A.2 Implementation Pattern
|
|
609
|
+
|
|
610
|
+
**Check for:**
|
|
611
|
+
- [ ] API client defined as a `struct` with `async` closure properties (not a singleton class)
|
|
612
|
+
- [ ] Static factory `.live(baseURL:)` for production
|
|
613
|
+
- [ ] Static factory `.mock(...)` for previews and tests
|
|
614
|
+
- [ ] Store (`@Observable`) holds the client; views never call the client directly
|
|
615
|
+
- [ ] Client injected via `@Environment` or constructor
|
|
616
|
+
|
|
617
|
+
**Examples:**
|
|
618
|
+
|
|
619
|
+
❌ **Bad: Singleton class client**
|
|
620
|
+
```swift
|
|
621
|
+
final class UserAPIClient {
|
|
622
|
+
static let shared = UserAPIClient() // ❌ Singleton — untestable, preview-unfriendly
|
|
623
|
+
|
|
624
|
+
func fetchUser(id: UUID) async throws -> User {
|
|
625
|
+
let url = URL(string: "https://api.example.com/users/\(id)")!
|
|
626
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
627
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
final class UserViewModel {
|
|
632
|
+
func loadUser(id: UUID) async {
|
|
633
|
+
let user = try await UserAPIClient.shared.fetchUser(id: id) // ❌ Hard dependency
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
✅ **Good: Struct with closure properties**
|
|
639
|
+
```swift
|
|
640
|
+
// Client defined as a struct with closure properties
|
|
641
|
+
struct UserAPIClient {
|
|
642
|
+
var fetchUser: (UUID) async throws -> User
|
|
643
|
+
var updateUser: (User) async throws -> User
|
|
644
|
+
var deleteUser: (UUID) async throws -> Void
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Live implementation
|
|
648
|
+
extension UserAPIClient {
|
|
649
|
+
static func live(baseURL: URL) -> Self {
|
|
650
|
+
UserAPIClient(
|
|
651
|
+
fetchUser: { id in
|
|
652
|
+
let url = baseURL.appendingPathComponent("users/\(id)")
|
|
653
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
654
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
655
|
+
},
|
|
656
|
+
updateUser: { user in
|
|
657
|
+
var request = URLRequest(url: baseURL.appendingPathComponent("users/\(user.id)"))
|
|
658
|
+
request.httpMethod = "PUT"
|
|
659
|
+
request.httpBody = try JSONEncoder().encode(user)
|
|
660
|
+
let (data, _) = try await URLSession.shared.data(for: request)
|
|
661
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
662
|
+
},
|
|
663
|
+
deleteUser: { id in
|
|
664
|
+
var request = URLRequest(url: baseURL.appendingPathComponent("users/\(id)"))
|
|
665
|
+
request.httpMethod = "DELETE"
|
|
666
|
+
_ = try await URLSession.shared.data(for: request)
|
|
667
|
+
}
|
|
668
|
+
)
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Mock implementation for previews and tests
|
|
673
|
+
extension UserAPIClient {
|
|
674
|
+
static func mock(
|
|
675
|
+
fetchUser: @escaping (UUID) async throws -> User = { _ in .mock },
|
|
676
|
+
updateUser: @escaping (User) async throws -> User = { $0 },
|
|
677
|
+
deleteUser: @escaping (UUID) async throws -> Void = { _ in }
|
|
678
|
+
) -> Self {
|
|
679
|
+
UserAPIClient(
|
|
680
|
+
fetchUser: fetchUser,
|
|
681
|
+
updateUser: updateUser,
|
|
682
|
+
deleteUser: deleteUser
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Store holds the client — views never call it directly
|
|
688
|
+
@MainActor
|
|
689
|
+
@Observable
|
|
690
|
+
final class UserStore {
|
|
691
|
+
private let client: UserAPIClient
|
|
692
|
+
private(set) var user: User?
|
|
693
|
+
private(set) var isLoading = false
|
|
694
|
+
|
|
695
|
+
init(client: UserAPIClient) {
|
|
696
|
+
self.client = client
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
func loadUser(id: UUID) async {
|
|
700
|
+
isLoading = true
|
|
701
|
+
defer { isLoading = false }
|
|
702
|
+
user = try? await client.fetchUser(id)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// View uses Store, not Client directly
|
|
707
|
+
struct UserProfileView: View {
|
|
708
|
+
let store: UserStore
|
|
709
|
+
|
|
710
|
+
var body: some View {
|
|
711
|
+
Group {
|
|
712
|
+
if let user = store.user {
|
|
713
|
+
Text(user.name)
|
|
714
|
+
} else {
|
|
715
|
+
ProgressView()
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
.task { await store.loadUser(id: userID) }
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// Preview uses mock client
|
|
723
|
+
#Preview {
|
|
724
|
+
UserProfileView(store: UserStore(client: .mock(
|
|
725
|
+
fetchUser: { _ in User(id: UUID(), name: "Preview User") }
|
|
726
|
+
)))
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// App uses live client
|
|
730
|
+
@main
|
|
731
|
+
struct MyApp: App {
|
|
732
|
+
let userStore = UserStore(client: .live(baseURL: URL(string: "https://api.example.com")!))
|
|
733
|
+
|
|
734
|
+
var body: some Scene {
|
|
735
|
+
WindowGroup {
|
|
736
|
+
UserProfileView(store: userStore)
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
---
|
|
743
|
+
|
|
744
|
+
## 5B. Per-Tab Navigation Architecture
|
|
745
|
+
|
|
746
|
+
### 5B.1 Overview
|
|
747
|
+
|
|
748
|
+
**Purpose**: Provide each tab with its own independent navigation stack and router, preserving navigation history across tab switches and supporting deep link routing to a specific tab.
|
|
749
|
+
|
|
750
|
+
**Benefits:**
|
|
751
|
+
- Tab navigation state preserved when switching tabs (native iOS behavior)
|
|
752
|
+
- Deep links can target specific tabs without resetting all stacks
|
|
753
|
+
- Decoupled tab-specific routing logic
|
|
754
|
+
|
|
755
|
+
### 5B.2 Implementation Pattern
|
|
756
|
+
|
|
757
|
+
**Check for:**
|
|
758
|
+
- [ ] `TabRouter` (or `AppTabRouter`) owns one `RouterPath` per tab
|
|
759
|
+
- [ ] `Binding(for tab:)` helper creates a `Binding<[Route]>` for each tab's stack
|
|
760
|
+
- [ ] Deep links dispatched to the correct tab's router (not a global path)
|
|
761
|
+
- [ ] Tab switching does not reset other tabs' navigation stacks
|
|
762
|
+
|
|
763
|
+
**Examples:**
|
|
764
|
+
|
|
765
|
+
❌ **Bad: Single shared path for all tabs**
|
|
766
|
+
```swift
|
|
767
|
+
struct MainTabView: View {
|
|
768
|
+
@State private var path = NavigationPath() // ❌ Shared — tab switch loses history
|
|
769
|
+
@State private var selectedTab = 0
|
|
770
|
+
|
|
771
|
+
var body: some View {
|
|
772
|
+
TabView(selection: $selectedTab) {
|
|
773
|
+
NavigationStack(path: $path) { HomeView() }.tag(0)
|
|
774
|
+
NavigationStack(path: $path) { SearchView() }.tag(1) // ❌ Same path!
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
✅ **Good: Per-tab RouterPath with deep link routing**
|
|
781
|
+
```swift
|
|
782
|
+
@Observable
|
|
783
|
+
final class RouterPath {
|
|
784
|
+
var path: [AppRoute] = []
|
|
785
|
+
|
|
786
|
+
func navigate(to route: AppRoute) { path.append(route) }
|
|
787
|
+
func pop() { if !path.isEmpty { path.removeLast() } }
|
|
788
|
+
func popToRoot() { path.removeAll() }
|
|
789
|
+
|
|
790
|
+
func handle(url: URL) -> Bool {
|
|
791
|
+
guard let route = AppRoute(url: url) else { return false }
|
|
792
|
+
navigate(to: route)
|
|
793
|
+
return true
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
enum AppTab: Int, CaseIterable, Identifiable {
|
|
798
|
+
case home, search, notifications, profile
|
|
799
|
+
var id: Int { rawValue }
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
@Observable
|
|
803
|
+
final class AppTabRouter {
|
|
804
|
+
var selectedTab: AppTab = .home
|
|
805
|
+
|
|
806
|
+
var homeRouter = RouterPath()
|
|
807
|
+
var searchRouter = RouterPath()
|
|
808
|
+
var notificationsRouter = RouterPath()
|
|
809
|
+
var profileRouter = RouterPath()
|
|
810
|
+
|
|
811
|
+
// Binding helper for each tab's path
|
|
812
|
+
func pathBinding(for tab: AppTab) -> Binding<[AppRoute]> {
|
|
813
|
+
Binding(
|
|
814
|
+
get: { self.router(for: tab).path },
|
|
815
|
+
set: { self.router(for: tab).path = $0 }
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
func router(for tab: AppTab) -> RouterPath {
|
|
820
|
+
switch tab {
|
|
821
|
+
case .home: return homeRouter
|
|
822
|
+
case .search: return searchRouter
|
|
823
|
+
case .notifications: return notificationsRouter
|
|
824
|
+
case .profile: return profileRouter
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
// Route deep link to correct tab
|
|
829
|
+
func handle(url: URL) {
|
|
830
|
+
guard let route = AppRoute(url: url) else { return }
|
|
831
|
+
let targetTab = route.preferredTab
|
|
832
|
+
selectedTab = targetTab
|
|
833
|
+
router(for: targetTab).navigate(to: route)
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
struct MainTabView: View {
|
|
838
|
+
@State private var tabRouter = AppTabRouter()
|
|
839
|
+
|
|
840
|
+
var body: some View {
|
|
841
|
+
TabView(selection: $tabRouter.selectedTab) {
|
|
842
|
+
Tab("Home", systemImage: "house", value: AppTab.home) {
|
|
843
|
+
NavigationStack(path: tabRouter.pathBinding(for: .home)) {
|
|
844
|
+
HomeView(router: tabRouter.homeRouter)
|
|
845
|
+
.navigationDestination(for: AppRoute.self) { route in
|
|
846
|
+
routeDestination(route, router: tabRouter.homeRouter)
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
Tab("Search", systemImage: "magnifyingglass", value: AppTab.search) {
|
|
852
|
+
NavigationStack(path: tabRouter.pathBinding(for: .search)) {
|
|
853
|
+
SearchView(router: tabRouter.searchRouter)
|
|
854
|
+
.navigationDestination(for: AppRoute.self) { route in
|
|
855
|
+
routeDestination(route, router: tabRouter.searchRouter)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// ... other tabs
|
|
861
|
+
}
|
|
862
|
+
.onOpenURL { url in
|
|
863
|
+
tabRouter.handle(url: url) // ✅ Deep link dispatched to correct tab router
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
596
871
|
## 6. Testing Strategies
|
|
597
872
|
|
|
598
873
|
### 6.1 Unit Testing
|