swift-code-reviewer-skill 1.0.0 → 1.1.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.
- package/CHANGELOG.md +7 -0
- package/README.md +56 -12
- package/SKILL.md +43 -3
- 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/CHANGELOG.md
CHANGED
|
@@ -157,6 +157,12 @@ Provided complete examples for:
|
|
|
157
157
|
- File-specific reviews
|
|
158
158
|
- Multi-file reviews
|
|
159
159
|
|
|
160
|
+
## [1.1.0] - 2026-03-16
|
|
161
|
+
|
|
162
|
+
### Added
|
|
163
|
+
|
|
164
|
+
- increase adjusts from Dimillian skill and more scenarios to cover
|
|
165
|
+
|
|
160
166
|
## [Unreleased]
|
|
161
167
|
|
|
162
168
|
### Planned Features
|
|
@@ -181,6 +187,7 @@ Provided complete examples for:
|
|
|
181
187
|
|
|
182
188
|
## Version History Summary
|
|
183
189
|
|
|
190
|
+
- **1.1.0** (2026-03-16): Increase adjusts from Dimillian skill and more scenarios to cover
|
|
184
191
|
- **1.0.0** (2026-02-10): Initial release with comprehensive review capabilities
|
|
185
192
|
|
|
186
193
|
---
|
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Swift Code Reviewer Agent Skill
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sponsors/Viniciuscarvalho)
|
|
4
|
+
|
|
3
5
|
A comprehensive code review skill for Claude that performs multi-layer analysis of Swift and SwiftUI code, combining Apple's best practices with project-specific coding standards.
|
|
4
6
|
|
|
5
7
|
## Features
|
|
@@ -14,6 +16,7 @@ A comprehensive code review skill for Claude that performs multi-layer analysis
|
|
|
14
16
|
## What It Reviews
|
|
15
17
|
|
|
16
18
|
### Swift Quality
|
|
19
|
+
|
|
17
20
|
- **Concurrency Safety**: Actor isolation, MainActor usage, Sendable conformance, data race prevention
|
|
18
21
|
- **Error Handling**: Typed throws, Result types, proper error propagation
|
|
19
22
|
- **Optionals**: Safe unwrapping, guard statements, no force unwraps
|
|
@@ -21,6 +24,7 @@ A comprehensive code review skill for Claude that performs multi-layer analysis
|
|
|
21
24
|
- **Naming**: Swift API Design Guidelines compliance
|
|
22
25
|
|
|
23
26
|
### SwiftUI Patterns
|
|
27
|
+
|
|
24
28
|
- **State Management**: @Observable, @State, @Binding, @Environment usage
|
|
25
29
|
- **Property Wrappers**: Correct wrapper selection for each use case
|
|
26
30
|
- **Modern APIs**: NavigationStack, .task, latest SwiftUI features
|
|
@@ -28,24 +32,28 @@ A comprehensive code review skill for Claude that performs multi-layer analysis
|
|
|
28
32
|
- **Accessibility**: Labels, hints, Dynamic Type support
|
|
29
33
|
|
|
30
34
|
### Performance
|
|
35
|
+
|
|
31
36
|
- **View Optimization**: Unnecessary updates, Equatable conformance
|
|
32
37
|
- **ForEach Performance**: Stable identity, lazy loading
|
|
33
38
|
- **Layout Efficiency**: GeometryReader usage, layout thrash
|
|
34
39
|
- **Resource Management**: Image loading, memory leaks, async patterns
|
|
35
40
|
|
|
36
41
|
### Security & Safety
|
|
42
|
+
|
|
37
43
|
- **Input Validation**: Sanitization, bounds checking, type safety
|
|
38
44
|
- **Sensitive Data**: Keychain storage, biometric authentication, secure logging
|
|
39
45
|
- **Network Security**: HTTPS enforcement, certificate pinning, API key protection
|
|
40
46
|
- **Permissions**: Privacy descriptions, permission timing, graceful handling
|
|
41
47
|
|
|
42
48
|
### Architecture
|
|
49
|
+
|
|
43
50
|
- **Pattern Compliance**: MVVM, MVI, TCA adherence
|
|
44
51
|
- **Dependency Injection**: Constructor injection, protocol-based dependencies
|
|
45
52
|
- **Code Organization**: File structure, MARK comments, logical grouping
|
|
46
53
|
- **Testability**: Unit test coverage, mock usage, test structure
|
|
47
54
|
|
|
48
55
|
### Project Standards
|
|
56
|
+
|
|
49
57
|
- **Custom Guidelines**: Validates against `.claude/CLAUDE.md` rules
|
|
50
58
|
- **Design System**: Color palette, typography, spacing token usage
|
|
51
59
|
- **Error Patterns**: Custom error type conformance
|
|
@@ -55,7 +63,7 @@ A comprehensive code review skill for Claude that performs multi-layer analysis
|
|
|
55
63
|
|
|
56
64
|
### Option 1: NPX (Recommended)
|
|
57
65
|
|
|
58
|
-
The fastest way to install
|
|
66
|
+
The fastest way to install — always fetches the latest version, no cloning or manual updates required:
|
|
59
67
|
|
|
60
68
|
```bash
|
|
61
69
|
npx swift-code-reviewer-skill
|
|
@@ -63,6 +71,8 @@ npx swift-code-reviewer-skill
|
|
|
63
71
|
|
|
64
72
|
This automatically installs the skill to `~/.claude/skills/swift-code-reviewer-skill/`
|
|
65
73
|
|
|
74
|
+
> **Tip:** Running `npx swift-code-reviewer-skill` again at any time will update to the latest version automatically.
|
|
75
|
+
|
|
66
76
|
To uninstall:
|
|
67
77
|
|
|
68
78
|
```bash
|
|
@@ -81,6 +91,7 @@ git clone https://github.com/Viniciuscarvalho/swift-code-reviewer-skill.git ~/.c
|
|
|
81
91
|
### Option 3: Manual Installation
|
|
82
92
|
|
|
83
93
|
1. Create the skill directory:
|
|
94
|
+
|
|
84
95
|
```bash
|
|
85
96
|
mkdir -p ~/.claude/skills/swift-code-reviewer-skill/references
|
|
86
97
|
```
|
|
@@ -98,6 +109,8 @@ ls ~/.claude/skills/swift-code-reviewer-skill/
|
|
|
98
109
|
|
|
99
110
|
## Usage
|
|
100
111
|
|
|
112
|
+

|
|
113
|
+
|
|
101
114
|
### Basic Usage
|
|
102
115
|
|
|
103
116
|
The skill automatically activates when you ask Claude to review Swift/SwiftUI code:
|
|
@@ -119,6 +132,7 @@ Review UserProfileView.swift
|
|
|
119
132
|
```
|
|
120
133
|
|
|
121
134
|
**What it does:**
|
|
135
|
+
|
|
122
136
|
1. Reads `.claude/CLAUDE.md` for project standards
|
|
123
137
|
2. Analyzes the file against all quality dimensions
|
|
124
138
|
3. Provides structured feedback with severity levels
|
|
@@ -131,6 +145,7 @@ Review my uncommitted changes
|
|
|
131
145
|
```
|
|
132
146
|
|
|
133
147
|
**What it does:**
|
|
148
|
+
|
|
134
149
|
1. Runs `git diff` to identify changes
|
|
135
150
|
2. Analyzes modified files
|
|
136
151
|
3. Focuses on changed lines for efficiency
|
|
@@ -143,6 +158,7 @@ Review PR #123
|
|
|
143
158
|
```
|
|
144
159
|
|
|
145
160
|
**What it does:**
|
|
161
|
+
|
|
146
162
|
1. Fetches PR details using `gh pr view 123`
|
|
147
163
|
2. Gets diff using `gh pr diff 123`
|
|
148
164
|
3. Reads all changed files for context
|
|
@@ -155,6 +171,7 @@ Review MR #456
|
|
|
155
171
|
```
|
|
156
172
|
|
|
157
173
|
**What it does:**
|
|
174
|
+
|
|
158
175
|
1. Fetches MR details using `glab mr view 456`
|
|
159
176
|
2. Gets diff and changed files
|
|
160
177
|
3. Performs multi-layer analysis
|
|
@@ -167,6 +184,7 @@ Review LoginViewModel.swift against our coding standards
|
|
|
167
184
|
```
|
|
168
185
|
|
|
169
186
|
**What it does:**
|
|
187
|
+
|
|
170
188
|
1. Reads `.claude/CLAUDE.md` and related architecture docs
|
|
171
189
|
2. Extracts project-specific rules
|
|
172
190
|
3. Validates code against both Apple and project standards
|
|
@@ -179,6 +197,7 @@ Review all ViewModels in the Features folder
|
|
|
179
197
|
```
|
|
180
198
|
|
|
181
199
|
**What it does:**
|
|
200
|
+
|
|
182
201
|
1. Finds all matching files
|
|
183
202
|
2. Analyzes each against architecture patterns
|
|
184
203
|
3. Provides file-by-file review
|
|
@@ -254,6 +273,7 @@ Generates a structured markdown report:
|
|
|
254
273
|
# Code Review Report
|
|
255
274
|
|
|
256
275
|
## Summary
|
|
276
|
+
|
|
257
277
|
- Files Reviewed: 5
|
|
258
278
|
- Critical: 0
|
|
259
279
|
- High: 2
|
|
@@ -266,15 +286,19 @@ Generates a structured markdown report:
|
|
|
266
286
|
### File: LoginView.swift
|
|
267
287
|
|
|
268
288
|
#### ✅ Positive Feedback
|
|
289
|
+
|
|
269
290
|
1. Excellent use of @Observable for state management
|
|
270
291
|
|
|
271
292
|
#### 🟡 High Priority
|
|
293
|
+
|
|
272
294
|
1. Force unwrap detected at line 89 (potential crash)
|
|
273
295
|
|
|
274
296
|
#### 💡 Refactoring Suggestions
|
|
297
|
+
|
|
275
298
|
1. Consider extracting login form into separate view
|
|
276
299
|
|
|
277
300
|
## Prioritized Action Items
|
|
301
|
+
|
|
278
302
|
[Must fix, should fix, consider items]
|
|
279
303
|
```
|
|
280
304
|
|
|
@@ -282,27 +306,30 @@ Generates a structured markdown report:
|
|
|
282
306
|
|
|
283
307
|
### Severity Levels
|
|
284
308
|
|
|
285
|
-
| Severity
|
|
286
|
-
|
|
287
|
-
| **Critical** | 🔴
|
|
288
|
-
| **High**
|
|
289
|
-
| **Medium**
|
|
290
|
-
| **Low**
|
|
309
|
+
| Severity | Icon | Description | Response Time |
|
|
310
|
+
| ------------ | ---- | --------------------------------------------------- | ----------------------- |
|
|
311
|
+
| **Critical** | 🔴 | Security vulnerabilities, crashes, data races | Must fix before merge |
|
|
312
|
+
| **High** | 🟡 | Performance issues, anti-patterns, major violations | Should fix before merge |
|
|
313
|
+
| **Medium** | 🟠 | Code quality, documentation, minor violations | Fix in current sprint |
|
|
314
|
+
| **Low** | 🔵 | Style, suggestions, minor improvements | Consider for future |
|
|
291
315
|
|
|
292
316
|
### Feedback Types
|
|
293
317
|
|
|
294
318
|
**Positive Feedback** ✅
|
|
319
|
+
|
|
295
320
|
- Acknowledges good practices
|
|
296
321
|
- Highlights excellent implementations
|
|
297
322
|
- Recognizes proper patterns
|
|
298
323
|
|
|
299
324
|
**Issues** 🔴🟡🟠🔵
|
|
325
|
+
|
|
300
326
|
- File and line references
|
|
301
327
|
- Code examples (before/after)
|
|
302
328
|
- Specific fixes with explanations
|
|
303
329
|
- Links to documentation
|
|
304
330
|
|
|
305
331
|
**Refactoring Suggestions** 💡
|
|
332
|
+
|
|
306
333
|
- Proactive improvements
|
|
307
334
|
- Modernization opportunities
|
|
308
335
|
- Code simplification ideas
|
|
@@ -321,32 +348,40 @@ The skill reads `.claude/CLAUDE.md` to understand your project's:
|
|
|
321
348
|
|
|
322
349
|
### Example .claude/CLAUDE.md
|
|
323
350
|
|
|
324
|
-
|
|
351
|
+
````markdown
|
|
325
352
|
# MyApp Coding Standards
|
|
326
353
|
|
|
327
354
|
## Architecture
|
|
355
|
+
|
|
328
356
|
We use **MVVM with Coordinators**:
|
|
357
|
+
|
|
329
358
|
- ViewModels MUST use @Observable (iOS 17+)
|
|
330
359
|
- All dependencies MUST be injected via constructor
|
|
331
360
|
- Views MUST NOT contain business logic
|
|
332
361
|
|
|
333
362
|
## Error Handling
|
|
363
|
+
|
|
334
364
|
All errors MUST conform to AppError:
|
|
365
|
+
|
|
335
366
|
```swift
|
|
336
367
|
protocol AppError: Error {
|
|
337
368
|
var message: String { get }
|
|
338
369
|
var code: Int { get }
|
|
339
370
|
}
|
|
340
371
|
```
|
|
372
|
+
````
|
|
341
373
|
|
|
342
374
|
## Design System
|
|
375
|
+
|
|
343
376
|
- Use AppColors enum ONLY
|
|
344
377
|
- Use AppFonts enum ONLY
|
|
345
378
|
- Use AppSpacing for all padding
|
|
346
379
|
|
|
347
380
|
## Testing
|
|
381
|
+
|
|
348
382
|
- Minimum coverage: 80%
|
|
349
383
|
- All ViewModels MUST have unit tests
|
|
384
|
+
|
|
350
385
|
```
|
|
351
386
|
|
|
352
387
|
The skill will validate your code against these standards and report violations.
|
|
@@ -391,8 +426,10 @@ For performance analysis
|
|
|
391
426
|
|
|
392
427
|
**Input:**
|
|
393
428
|
```
|
|
429
|
+
|
|
394
430
|
Review my uncommitted changes
|
|
395
|
-
|
|
431
|
+
|
|
432
|
+
````
|
|
396
433
|
|
|
397
434
|
**Output:**
|
|
398
435
|
```markdown
|
|
@@ -419,23 +456,27 @@ Review my uncommitted changes
|
|
|
419
456
|
Current Code:
|
|
420
457
|
```swift
|
|
421
458
|
let user = repository.currentUser!
|
|
422
|
-
|
|
459
|
+
````
|
|
423
460
|
|
|
424
461
|
Recommended Fix:
|
|
462
|
+
|
|
425
463
|
```swift
|
|
426
464
|
guard let user = repository.currentUser else {
|
|
427
465
|
logger.error("No current user")
|
|
428
466
|
return
|
|
429
467
|
}
|
|
430
468
|
```
|
|
469
|
+
|
|
431
470
|
```
|
|
432
471
|
|
|
433
472
|
### Example 2: Review Against Project Standards
|
|
434
473
|
|
|
435
474
|
**Input:**
|
|
436
475
|
```
|
|
476
|
+
|
|
437
477
|
Review LoginView.swift against our coding standards
|
|
438
|
-
|
|
478
|
+
|
|
479
|
+
````
|
|
439
480
|
|
|
440
481
|
**Output:**
|
|
441
482
|
```markdown
|
|
@@ -454,14 +495,16 @@ Review LoginView.swift against our coding standards
|
|
|
454
495
|
Current Code:
|
|
455
496
|
```swift
|
|
456
497
|
.foregroundColor(.blue) // ❌ Hardcoded
|
|
457
|
-
|
|
498
|
+
````
|
|
458
499
|
|
|
459
500
|
Expected Code:
|
|
501
|
+
|
|
460
502
|
```swift
|
|
461
503
|
.foregroundColor(AppColors.primary) // ✅ Design system
|
|
462
504
|
```
|
|
463
505
|
|
|
464
506
|
Reference: .claude/CLAUDE.md:Design System
|
|
507
|
+
|
|
465
508
|
```
|
|
466
509
|
|
|
467
510
|
## Requirements
|
|
@@ -534,3 +577,4 @@ MIT License - See [LICENSE](LICENSE) file for details
|
|
|
534
577
|
**Made with ❤️ for the Swift community**
|
|
535
578
|
|
|
536
579
|
If this skill helps improve your code reviews, please ⭐️ star the repository!
|
|
580
|
+
```
|
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
|
|
@@ -389,6 +389,23 @@ This skill **references** (not duplicates) three foundational skills for domain
|
|
|
389
389
|
- Reference performance-specific checks when reviewing view code
|
|
390
390
|
- Apply recommendations from the skill to performance-sensitive paths
|
|
391
391
|
|
|
392
|
+
### 4. swiftui-ui-patterns
|
|
393
|
+
**When to Use:** Reviewing navigation architecture, sheet/modal routing, TabView setup, theming, async state management, focus handling, or API client patterns
|
|
394
|
+
|
|
395
|
+
**What it Provides:**
|
|
396
|
+
- Navigation architecture (route enums, RouterPath, centralized navigationDestination)
|
|
397
|
+
- Sheet/modal routing (item-driven sheets, SheetDestination enum)
|
|
398
|
+
- TabView with independent per-tab navigation history
|
|
399
|
+
- Theming with semantic colors via `@Environment(Theme.self)`
|
|
400
|
+
- Async state patterns (`.task(id:)`, LoadState enum, CancellationError handling)
|
|
401
|
+
- Focus chaining with FocusField enum and `.onSubmit`
|
|
402
|
+
- Lightweight API client pattern (closure-based structs, `.live()` / `.mock()` factories)
|
|
403
|
+
|
|
404
|
+
**How to Leverage:**
|
|
405
|
+
- Read `~/.claude/skills/swiftui-ui-patterns/references/navigation.md` for route enum and RouterPath checks
|
|
406
|
+
- Reference `sheets-modals.md` for sheet routing validation
|
|
407
|
+
- Use `theming.md` for semantic color enforcement
|
|
408
|
+
|
|
392
409
|
**Integration Strategy:**
|
|
393
410
|
1. Load relevant reference files from these skills as needed
|
|
394
411
|
2. Apply their checklist items to the review
|
|
@@ -619,6 +636,29 @@ Steps:
|
|
|
619
636
|
5. Summarize common patterns and issues across all files
|
|
620
637
|
```
|
|
621
638
|
|
|
639
|
+
### Example 6: Review Navigation / Routing Code
|
|
640
|
+
```
|
|
641
|
+
User: "Review our navigation setup and routing code"
|
|
642
|
+
|
|
643
|
+
Steps:
|
|
644
|
+
1. Read .claude/CLAUDE.md for project navigation patterns
|
|
645
|
+
2. Read router/coordinator files (RouterPath, AppCoordinator, TabRouter)
|
|
646
|
+
3. Read root views that set up NavigationStack and TabView
|
|
647
|
+
4. Run navigation architecture checks:
|
|
648
|
+
- Route destinations use typed Hashable enum (not String/Int)
|
|
649
|
+
- RouterPath @Observable owns path (not ad-hoc @State)
|
|
650
|
+
- Single centralized .navigationDestination per stack
|
|
651
|
+
- .sheet(item:) preferred over .sheet(isPresented:) when model selected
|
|
652
|
+
- Multiple sheets use SheetDestination enum (not multiple booleans)
|
|
653
|
+
- Each tab has independent RouterPath (not shared)
|
|
654
|
+
- .onOpenURL at app root, not scattered in feature views
|
|
655
|
+
5. Run async state checks:
|
|
656
|
+
- .task(id:) for input-driven async work
|
|
657
|
+
- CancellationError silenced
|
|
658
|
+
- LoadState<T> enum instead of multiple booleans
|
|
659
|
+
6. Generate report with navigation-specific findings
|
|
660
|
+
```
|
|
661
|
+
|
|
622
662
|
## Resources
|
|
623
663
|
|
|
624
664
|
This skill includes the following reference materials:
|
|
@@ -685,6 +725,6 @@ For runtime analysis, recommend using Instruments or other profiling tools.
|
|
|
685
725
|
|
|
686
726
|
## Version
|
|
687
727
|
|
|
688
|
-
**Version**: 1.
|
|
689
|
-
**Last Updated**: 2026-
|
|
728
|
+
**Version**: 1.1.0
|
|
729
|
+
**Last Updated**: 2026-03-16
|
|
690
730
|
**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
|