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.
- package/CHANGELOG.md +214 -0
- package/CONTRIBUTING.md +271 -0
- package/LICENSE +21 -0
- package/README.md +536 -0
- package/SKILL.md +690 -0
- package/bin/install.js +173 -0
- package/package.json +41 -0
- package/references/architecture-patterns.md +862 -0
- package/references/custom-guidelines.md +852 -0
- package/references/feedback-templates.md +666 -0
- package/references/performance-review.md +914 -0
- package/references/review-workflow.md +1131 -0
- package/references/security-checklist.md +781 -0
- package/references/swift-quality-checklist.md +928 -0
- package/references/swiftui-review-checklist.md +909 -0
|
@@ -0,0 +1,862 @@
|
|
|
1
|
+
# Architecture Patterns Guide
|
|
2
|
+
|
|
3
|
+
This guide covers common architectural patterns for Swift and SwiftUI applications, including MVVM, MVI, TCA, dependency injection, testing strategies, and code organization principles.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. MVVM (Model-View-ViewModel)
|
|
8
|
+
|
|
9
|
+
### 1.1 Overview
|
|
10
|
+
|
|
11
|
+
**Structure:**
|
|
12
|
+
- **View**: SwiftUI views (presentation only)
|
|
13
|
+
- **ViewModel**: Business logic and state management
|
|
14
|
+
- **Model**: Data structures and domain logic
|
|
15
|
+
|
|
16
|
+
**Responsibilities:**
|
|
17
|
+
|
|
18
|
+
| Layer | Responsibilities | Does NOT Do |
|
|
19
|
+
|-------|-----------------|-------------|
|
|
20
|
+
| **View** | - Display data<br>- User interactions<br>- UI layout | - Business logic<br>- Data fetching<br>- Validation |
|
|
21
|
+
| **ViewModel** | - Business logic<br>- State management<br>- Data transformation<br>- Validation | - UI code<br>- View hierarchy<br>- Direct database/network |
|
|
22
|
+
| **Model** | - Data structures<br>- Domain logic<br>- Business rules | - UI concerns<br>- State management |
|
|
23
|
+
|
|
24
|
+
### 1.2 Implementation Pattern
|
|
25
|
+
|
|
26
|
+
**Check for:**
|
|
27
|
+
- [ ] Views only contain presentation logic
|
|
28
|
+
- [ ] ViewModels contain all business logic
|
|
29
|
+
- [ ] Clear separation between View and ViewModel
|
|
30
|
+
- [ ] Dependency injection for services
|
|
31
|
+
|
|
32
|
+
**Examples:**
|
|
33
|
+
|
|
34
|
+
❌ **Bad: Business logic in view**
|
|
35
|
+
```swift
|
|
36
|
+
struct UserListView: View {
|
|
37
|
+
@State private var users: [User] = []
|
|
38
|
+
@State private var isLoading = false
|
|
39
|
+
|
|
40
|
+
var body: some View {
|
|
41
|
+
List(users) { user in
|
|
42
|
+
UserRow(user: user)
|
|
43
|
+
}
|
|
44
|
+
.onAppear {
|
|
45
|
+
loadUsers() // ❌ Business logic in view
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
private func loadUsers() {
|
|
50
|
+
isLoading = true
|
|
51
|
+
Task {
|
|
52
|
+
// ❌ Network call directly in view
|
|
53
|
+
let response = try await URLSession.shared.data(from: usersURL)
|
|
54
|
+
users = try JSONDecoder().decode([User].self, from: response.0)
|
|
55
|
+
isLoading = false
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
✅ **Good: MVVM structure**
|
|
62
|
+
```swift
|
|
63
|
+
// Model
|
|
64
|
+
struct User: Identifiable, Codable {
|
|
65
|
+
let id: UUID
|
|
66
|
+
let name: String
|
|
67
|
+
let email: String
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ViewModel
|
|
71
|
+
@MainActor
|
|
72
|
+
@Observable
|
|
73
|
+
final class UserListViewModel {
|
|
74
|
+
private let userRepository: UserRepository
|
|
75
|
+
|
|
76
|
+
private(set) var users: [User] = []
|
|
77
|
+
private(set) var isLoading: Bool = false
|
|
78
|
+
private(set) var error: Error?
|
|
79
|
+
|
|
80
|
+
init(userRepository: UserRepository) {
|
|
81
|
+
self.userRepository = userRepository
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
func loadUsers() async {
|
|
85
|
+
isLoading = true
|
|
86
|
+
error = nil
|
|
87
|
+
|
|
88
|
+
do {
|
|
89
|
+
users = try await userRepository.fetchUsers()
|
|
90
|
+
} catch {
|
|
91
|
+
self.error = error
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
isLoading = false
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// View
|
|
99
|
+
struct UserListView: View {
|
|
100
|
+
let viewModel: UserListViewModel
|
|
101
|
+
|
|
102
|
+
var body: some View {
|
|
103
|
+
List(viewModel.users) { user in
|
|
104
|
+
UserRow(user: user)
|
|
105
|
+
}
|
|
106
|
+
.overlay {
|
|
107
|
+
if viewModel.isLoading {
|
|
108
|
+
ProgressView()
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
.task {
|
|
112
|
+
await viewModel.loadUsers() // ✅ View triggers, ViewModel handles
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 1.3 ViewModel Best Practices
|
|
119
|
+
|
|
120
|
+
**Check for:**
|
|
121
|
+
- [ ] ViewModels are @Observable (iOS 17+) or ObservableObject (iOS 16-)
|
|
122
|
+
- [ ] @MainActor for UI-related ViewModels
|
|
123
|
+
- [ ] Services injected via initializer
|
|
124
|
+
- [ ] ViewModels are testable (protocol-based dependencies)
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 2. Repository Pattern
|
|
129
|
+
|
|
130
|
+
### 2.1 Overview
|
|
131
|
+
|
|
132
|
+
**Purpose**: Abstracts data sources (network, database, cache) behind a clean interface
|
|
133
|
+
|
|
134
|
+
**Structure:**
|
|
135
|
+
- **Repository Protocol**: Defines data operations
|
|
136
|
+
- **Repository Implementation**: Implements protocol using data sources
|
|
137
|
+
- **Data Sources**: Network client, database, cache
|
|
138
|
+
|
|
139
|
+
### 2.2 Implementation Pattern
|
|
140
|
+
|
|
141
|
+
**Check for:**
|
|
142
|
+
- [ ] Repository protocols for abstraction
|
|
143
|
+
- [ ] Multiple data sources coordinated
|
|
144
|
+
- [ ] Caching strategy implemented
|
|
145
|
+
- [ ] Error handling at repository level
|
|
146
|
+
|
|
147
|
+
**Examples:**
|
|
148
|
+
|
|
149
|
+
✅ **Good: Repository pattern**
|
|
150
|
+
```swift
|
|
151
|
+
// Repository Protocol
|
|
152
|
+
protocol UserRepository {
|
|
153
|
+
func fetchUsers() async throws -> [User]
|
|
154
|
+
func fetchUser(id: UUID) async throws -> User
|
|
155
|
+
func saveUser(_ user: User) async throws
|
|
156
|
+
func deleteUser(id: UUID) async throws
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Repository Implementation
|
|
160
|
+
final class DefaultUserRepository: UserRepository {
|
|
161
|
+
private let networkClient: NetworkClient
|
|
162
|
+
private let database: Database
|
|
163
|
+
private let cache: Cache
|
|
164
|
+
|
|
165
|
+
init(
|
|
166
|
+
networkClient: NetworkClient,
|
|
167
|
+
database: Database,
|
|
168
|
+
cache: Cache
|
|
169
|
+
) {
|
|
170
|
+
self.networkClient = networkClient
|
|
171
|
+
self.database = database
|
|
172
|
+
self.cache = cache
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func fetchUsers() async throws -> [User] {
|
|
176
|
+
// Check cache first
|
|
177
|
+
if let cached = cache.users, !cached.isEmpty {
|
|
178
|
+
return cached
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Fetch from network
|
|
182
|
+
let users = try await networkClient.fetchUsers()
|
|
183
|
+
|
|
184
|
+
// Save to database and cache
|
|
185
|
+
try await database.save(users)
|
|
186
|
+
cache.users = users
|
|
187
|
+
|
|
188
|
+
return users
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
func fetchUser(id: UUID) async throws -> User {
|
|
192
|
+
// Check cache
|
|
193
|
+
if let cached = cache.user(id: id) {
|
|
194
|
+
return cached
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Check database
|
|
198
|
+
if let local = try await database.fetchUser(id: id) {
|
|
199
|
+
cache.setUser(local)
|
|
200
|
+
return local
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fetch from network
|
|
204
|
+
let user = try await networkClient.fetchUser(id: id)
|
|
205
|
+
|
|
206
|
+
// Save locally
|
|
207
|
+
try await database.save(user)
|
|
208
|
+
cache.setUser(user)
|
|
209
|
+
|
|
210
|
+
return user
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
func saveUser(_ user: User) async throws {
|
|
214
|
+
// Save to network first
|
|
215
|
+
try await networkClient.saveUser(user)
|
|
216
|
+
|
|
217
|
+
// Then save locally
|
|
218
|
+
try await database.save(user)
|
|
219
|
+
cache.setUser(user)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
func deleteUser(id: UUID) async throws {
|
|
223
|
+
// Delete from network
|
|
224
|
+
try await networkClient.deleteUser(id: id)
|
|
225
|
+
|
|
226
|
+
// Delete locally
|
|
227
|
+
try await database.deleteUser(id: id)
|
|
228
|
+
cache.removeUser(id: id)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
## 3. Dependency Injection
|
|
236
|
+
|
|
237
|
+
### 3.1 Constructor Injection
|
|
238
|
+
|
|
239
|
+
**Check for:**
|
|
240
|
+
- [ ] Dependencies passed via initializer
|
|
241
|
+
- [ ] Protocol-based dependencies (testable)
|
|
242
|
+
- [ ] No service locator or singletons
|
|
243
|
+
|
|
244
|
+
**Examples:**
|
|
245
|
+
|
|
246
|
+
❌ **Bad: Singleton dependency**
|
|
247
|
+
```swift
|
|
248
|
+
final class UserViewModel {
|
|
249
|
+
func loadUsers() async {
|
|
250
|
+
// ❌ Hard dependency on singleton
|
|
251
|
+
let users = try await NetworkService.shared.fetchUsers()
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
✅ **Good: Constructor injection**
|
|
257
|
+
```swift
|
|
258
|
+
final class UserViewModel {
|
|
259
|
+
private let userRepository: UserRepository // ✅ Protocol
|
|
260
|
+
|
|
261
|
+
init(userRepository: UserRepository) { // ✅ Injected
|
|
262
|
+
self.userRepository = userRepository
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
func loadUsers() async {
|
|
266
|
+
let users = try await userRepository.fetchUsers()
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Usage
|
|
271
|
+
let viewModel = UserViewModel(
|
|
272
|
+
userRepository: DefaultUserRepository(
|
|
273
|
+
networkClient: networkClient,
|
|
274
|
+
database: database,
|
|
275
|
+
cache: cache
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### 3.2 Environment-Based Injection (SwiftUI)
|
|
281
|
+
|
|
282
|
+
**Check for:**
|
|
283
|
+
- [ ] Custom environment values for dependencies
|
|
284
|
+
- [ ] Environment values for cross-cutting concerns
|
|
285
|
+
- [ ] Proper dependency scoping
|
|
286
|
+
|
|
287
|
+
**Examples:**
|
|
288
|
+
|
|
289
|
+
✅ **Good: Environment injection**
|
|
290
|
+
```swift
|
|
291
|
+
// Define environment key
|
|
292
|
+
private struct UserRepositoryKey: EnvironmentKey {
|
|
293
|
+
static let defaultValue: UserRepository = MockUserRepository()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
extension EnvironmentValues {
|
|
297
|
+
var userRepository: UserRepository {
|
|
298
|
+
get { self[UserRepositoryKey.self] }
|
|
299
|
+
set { self[UserRepositoryKey.self] = newValue }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Provide in app
|
|
304
|
+
@main
|
|
305
|
+
struct MyApp: App {
|
|
306
|
+
let userRepository = DefaultUserRepository(...)
|
|
307
|
+
|
|
308
|
+
var body: some Scene {
|
|
309
|
+
WindowGroup {
|
|
310
|
+
ContentView()
|
|
311
|
+
.environment(\.userRepository, userRepository)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Use in view
|
|
317
|
+
struct UserListView: View {
|
|
318
|
+
@Environment(\.userRepository) private var repository
|
|
319
|
+
@State private var users: [User] = []
|
|
320
|
+
|
|
321
|
+
var body: some View {
|
|
322
|
+
List(users) { user in
|
|
323
|
+
UserRow(user: user)
|
|
324
|
+
}
|
|
325
|
+
.task {
|
|
326
|
+
users = try await repository.fetchUsers()
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### 3.3 Dependency Container
|
|
333
|
+
|
|
334
|
+
**Check for:**
|
|
335
|
+
- [ ] Centralized dependency registration
|
|
336
|
+
- [ ] Type-safe dependency resolution
|
|
337
|
+
- [ ] Proper scoping (singleton, transient, scoped)
|
|
338
|
+
|
|
339
|
+
**Examples:**
|
|
340
|
+
|
|
341
|
+
✅ **Good: Simple dependency container**
|
|
342
|
+
```swift
|
|
343
|
+
final class DependencyContainer {
|
|
344
|
+
static let shared = DependencyContainer()
|
|
345
|
+
|
|
346
|
+
// Singletons
|
|
347
|
+
lazy var networkClient: NetworkClient = {
|
|
348
|
+
DefaultNetworkClient(configuration: .default)
|
|
349
|
+
}()
|
|
350
|
+
|
|
351
|
+
lazy var database: Database = {
|
|
352
|
+
try! Database(path: databasePath)
|
|
353
|
+
}()
|
|
354
|
+
|
|
355
|
+
lazy var cache: Cache = {
|
|
356
|
+
InMemoryCache()
|
|
357
|
+
}()
|
|
358
|
+
|
|
359
|
+
// Factories
|
|
360
|
+
func makeUserRepository() -> UserRepository {
|
|
361
|
+
DefaultUserRepository(
|
|
362
|
+
networkClient: networkClient,
|
|
363
|
+
database: database,
|
|
364
|
+
cache: cache
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
func makeUserViewModel() -> UserListViewModel {
|
|
369
|
+
UserListViewModel(userRepository: makeUserRepository())
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Usage
|
|
374
|
+
let viewModel = DependencyContainer.shared.makeUserViewModel()
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
## 4. Use Case / Interactor Pattern
|
|
380
|
+
|
|
381
|
+
### 4.1 Overview
|
|
382
|
+
|
|
383
|
+
**Purpose**: Encapsulates a single business operation or use case
|
|
384
|
+
|
|
385
|
+
**Benefits:**
|
|
386
|
+
- Single Responsibility Principle
|
|
387
|
+
- Easy to test
|
|
388
|
+
- Reusable across multiple ViewModels
|
|
389
|
+
- Clear business logic separation
|
|
390
|
+
|
|
391
|
+
### 4.2 Implementation Pattern
|
|
392
|
+
|
|
393
|
+
**Check for:**
|
|
394
|
+
- [ ] One use case per class
|
|
395
|
+
- [ ] Execute method for operation
|
|
396
|
+
- [ ] Dependencies injected
|
|
397
|
+
- [ ] Returns Result or throws
|
|
398
|
+
|
|
399
|
+
**Examples:**
|
|
400
|
+
|
|
401
|
+
✅ **Good: Use case pattern**
|
|
402
|
+
```swift
|
|
403
|
+
// Use case protocol
|
|
404
|
+
protocol UseCase {
|
|
405
|
+
associatedtype Input
|
|
406
|
+
associatedtype Output
|
|
407
|
+
|
|
408
|
+
func execute(_ input: Input) async throws -> Output
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Login use case
|
|
412
|
+
struct LoginUseCase: UseCase {
|
|
413
|
+
private let authRepository: AuthRepository
|
|
414
|
+
private let tokenStorage: TokenStorage
|
|
415
|
+
|
|
416
|
+
init(authRepository: AuthRepository, tokenStorage: TokenStorage) {
|
|
417
|
+
self.authRepository = authRepository
|
|
418
|
+
self.tokenStorage = tokenStorage
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
struct Input {
|
|
422
|
+
let email: String
|
|
423
|
+
let password: String
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
func execute(_ input: Input) async throws -> User {
|
|
427
|
+
// Validate input
|
|
428
|
+
guard validateEmail(input.email) else {
|
|
429
|
+
throw LoginError.invalidEmail
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
guard input.password.count >= 8 else {
|
|
433
|
+
throw LoginError.passwordTooShort
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Perform login
|
|
437
|
+
let response = try await authRepository.login(
|
|
438
|
+
email: input.email,
|
|
439
|
+
password: input.password
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
// Store token
|
|
443
|
+
try await tokenStorage.save(response.token)
|
|
444
|
+
|
|
445
|
+
return response.user
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private func validateEmail(_ email: String) -> Bool {
|
|
449
|
+
// Email validation logic
|
|
450
|
+
true
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// ViewModel using use case
|
|
455
|
+
@MainActor
|
|
456
|
+
@Observable
|
|
457
|
+
final class LoginViewModel {
|
|
458
|
+
private let loginUseCase: LoginUseCase
|
|
459
|
+
|
|
460
|
+
var email: String = ""
|
|
461
|
+
var password: String = ""
|
|
462
|
+
var isLoading: Bool = false
|
|
463
|
+
var error: Error?
|
|
464
|
+
|
|
465
|
+
init(loginUseCase: LoginUseCase) {
|
|
466
|
+
self.loginUseCase = loginUseCase
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
func login() async {
|
|
470
|
+
isLoading = true
|
|
471
|
+
error = nil
|
|
472
|
+
|
|
473
|
+
do {
|
|
474
|
+
let input = LoginUseCase.Input(email: email, password: password)
|
|
475
|
+
let user = try await loginUseCase.execute(input)
|
|
476
|
+
// Handle successful login
|
|
477
|
+
} catch {
|
|
478
|
+
self.error = error
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
isLoading = false
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## 5. Coordinator Pattern
|
|
489
|
+
|
|
490
|
+
### 5.1 Overview
|
|
491
|
+
|
|
492
|
+
**Purpose**: Manages navigation flow and screen transitions
|
|
493
|
+
|
|
494
|
+
**Benefits:**
|
|
495
|
+
- Decouples navigation from views
|
|
496
|
+
- Centralized navigation logic
|
|
497
|
+
- Deep linking support
|
|
498
|
+
- Testing navigation flows
|
|
499
|
+
|
|
500
|
+
### 5.2 Implementation Pattern
|
|
501
|
+
|
|
502
|
+
**Check for:**
|
|
503
|
+
- [ ] Coordinator manages navigation state
|
|
504
|
+
- [ ] Views don't handle navigation
|
|
505
|
+
- [ ] Type-safe navigation
|
|
506
|
+
- [ ] Support for deep linking
|
|
507
|
+
|
|
508
|
+
**Examples:**
|
|
509
|
+
|
|
510
|
+
✅ **Good: Coordinator pattern**
|
|
511
|
+
```swift
|
|
512
|
+
// Route definition
|
|
513
|
+
enum Route: Hashable {
|
|
514
|
+
case userList
|
|
515
|
+
case userDetail(User)
|
|
516
|
+
case userEdit(User)
|
|
517
|
+
case settings
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Coordinator
|
|
521
|
+
@MainActor
|
|
522
|
+
@Observable
|
|
523
|
+
final class AppCoordinator {
|
|
524
|
+
var navigationPath = NavigationPath()
|
|
525
|
+
|
|
526
|
+
func navigate(to route: Route) {
|
|
527
|
+
navigationPath.append(route)
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
func pop() {
|
|
531
|
+
guard !navigationPath.isEmpty else { return }
|
|
532
|
+
navigationPath.removeLast()
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
func popToRoot() {
|
|
536
|
+
navigationPath.removeLast(navigationPath.count)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Root view with navigation
|
|
541
|
+
struct AppView: View {
|
|
542
|
+
@State private var coordinator = AppCoordinator()
|
|
543
|
+
|
|
544
|
+
var body: some View {
|
|
545
|
+
NavigationStack(path: $coordinator.navigationPath) {
|
|
546
|
+
UserListView(coordinator: coordinator)
|
|
547
|
+
.navigationDestination(for: Route.self) { route in
|
|
548
|
+
destination(for: route)
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
@ViewBuilder
|
|
554
|
+
private func destination(for route: Route) -> some View {
|
|
555
|
+
switch route {
|
|
556
|
+
case .userList:
|
|
557
|
+
UserListView(coordinator: coordinator)
|
|
558
|
+
|
|
559
|
+
case .userDetail(let user):
|
|
560
|
+
UserDetailView(user: user, coordinator: coordinator)
|
|
561
|
+
|
|
562
|
+
case .userEdit(let user):
|
|
563
|
+
UserEditView(user: user, coordinator: coordinator)
|
|
564
|
+
|
|
565
|
+
case .settings:
|
|
566
|
+
SettingsView(coordinator: coordinator)
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// View using coordinator
|
|
572
|
+
struct UserListView: View {
|
|
573
|
+
let coordinator: AppCoordinator
|
|
574
|
+
@State private var users: [User] = []
|
|
575
|
+
|
|
576
|
+
var body: some View {
|
|
577
|
+
List(users) { user in
|
|
578
|
+
Button {
|
|
579
|
+
coordinator.navigate(to: .userDetail(user)) // ✅ Coordinator handles navigation
|
|
580
|
+
} label: {
|
|
581
|
+
UserRow(user: user)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
.navigationTitle("Users")
|
|
585
|
+
.toolbar {
|
|
586
|
+
Button("Settings") {
|
|
587
|
+
coordinator.navigate(to: .settings)
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
---
|
|
595
|
+
|
|
596
|
+
## 6. Testing Strategies
|
|
597
|
+
|
|
598
|
+
### 6.1 Unit Testing
|
|
599
|
+
|
|
600
|
+
**Check for:**
|
|
601
|
+
- [ ] ViewModels are unit tested
|
|
602
|
+
- [ ] Use cases are unit tested
|
|
603
|
+
- [ ] Repositories are unit tested
|
|
604
|
+
- [ ] Mocks used for dependencies
|
|
605
|
+
- [ ] High code coverage (>80%)
|
|
606
|
+
|
|
607
|
+
**Examples:**
|
|
608
|
+
|
|
609
|
+
✅ **Good: Unit tests**
|
|
610
|
+
```swift
|
|
611
|
+
import XCTest
|
|
612
|
+
@testable import MyApp
|
|
613
|
+
|
|
614
|
+
final class LoginViewModelTests: XCTestCase {
|
|
615
|
+
private var mockAuthRepository: MockAuthRepository!
|
|
616
|
+
private var mockTokenStorage: MockTokenStorage!
|
|
617
|
+
private var loginUseCase: LoginUseCase!
|
|
618
|
+
private var viewModel: LoginViewModel!
|
|
619
|
+
|
|
620
|
+
@MainActor
|
|
621
|
+
override func setUp() {
|
|
622
|
+
super.setUp()
|
|
623
|
+
|
|
624
|
+
mockAuthRepository = MockAuthRepository()
|
|
625
|
+
mockTokenStorage = MockTokenStorage()
|
|
626
|
+
|
|
627
|
+
loginUseCase = LoginUseCase(
|
|
628
|
+
authRepository: mockAuthRepository,
|
|
629
|
+
tokenStorage: mockTokenStorage
|
|
630
|
+
)
|
|
631
|
+
|
|
632
|
+
viewModel = LoginViewModel(loginUseCase: loginUseCase)
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
@MainActor
|
|
636
|
+
func testSuccessfulLogin() async throws {
|
|
637
|
+
// Arrange
|
|
638
|
+
let expectedUser = User(id: UUID(), name: "John", email: "john@example.com")
|
|
639
|
+
mockAuthRepository.loginResult = .success(
|
|
640
|
+
LoginResponse(user: expectedUser, token: "token123")
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
viewModel.email = "john@example.com"
|
|
644
|
+
viewModel.password = "password123"
|
|
645
|
+
|
|
646
|
+
// Act
|
|
647
|
+
await viewModel.login()
|
|
648
|
+
|
|
649
|
+
// Assert
|
|
650
|
+
XCTAssertFalse(viewModel.isLoading)
|
|
651
|
+
XCTAssertNil(viewModel.error)
|
|
652
|
+
XCTAssertEqual(mockAuthRepository.loginCallCount, 1)
|
|
653
|
+
XCTAssertEqual(mockTokenStorage.savedToken, "token123")
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
@MainActor
|
|
657
|
+
func testLoginWithInvalidEmail() async {
|
|
658
|
+
// Arrange
|
|
659
|
+
viewModel.email = "invalid-email"
|
|
660
|
+
viewModel.password = "password123"
|
|
661
|
+
|
|
662
|
+
// Act
|
|
663
|
+
await viewModel.login()
|
|
664
|
+
|
|
665
|
+
// Assert
|
|
666
|
+
XCTAssertFalse(viewModel.isLoading)
|
|
667
|
+
XCTAssertNotNil(viewModel.error)
|
|
668
|
+
XCTAssertEqual(mockAuthRepository.loginCallCount, 0) // Not called
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Mock repository
|
|
673
|
+
final class MockAuthRepository: AuthRepository {
|
|
674
|
+
var loginResult: Result<LoginResponse, Error> = .failure(MockError.notImplemented)
|
|
675
|
+
var loginCallCount = 0
|
|
676
|
+
|
|
677
|
+
func login(email: String, password: String) async throws -> LoginResponse {
|
|
678
|
+
loginCallCount += 1
|
|
679
|
+
return try loginResult.get()
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
enum MockError: Error {
|
|
684
|
+
case notImplemented
|
|
685
|
+
}
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
### 6.2 UI Testing
|
|
689
|
+
|
|
690
|
+
**Check for:**
|
|
691
|
+
- [ ] Critical user flows tested
|
|
692
|
+
- [ ] Accessibility identifiers used
|
|
693
|
+
- [ ] Page Object pattern for organization
|
|
694
|
+
- [ ] Tests are maintainable
|
|
695
|
+
|
|
696
|
+
**Examples:**
|
|
697
|
+
|
|
698
|
+
✅ **Good: UI tests**
|
|
699
|
+
```swift
|
|
700
|
+
import XCTest
|
|
701
|
+
|
|
702
|
+
final class LoginUITests: XCTestCase {
|
|
703
|
+
private var app: XCUIApplication!
|
|
704
|
+
|
|
705
|
+
override func setUpWithError() throws {
|
|
706
|
+
continueAfterFailure = false
|
|
707
|
+
app = XCUIApplication()
|
|
708
|
+
app.launch()
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
func testLoginFlow() {
|
|
712
|
+
// Arrange
|
|
713
|
+
let emailField = app.textFields["loginEmailField"]
|
|
714
|
+
let passwordField = app.secureTextFields["loginPasswordField"]
|
|
715
|
+
let loginButton = app.buttons["loginButton"]
|
|
716
|
+
|
|
717
|
+
// Act
|
|
718
|
+
emailField.tap()
|
|
719
|
+
emailField.typeText("john@example.com")
|
|
720
|
+
|
|
721
|
+
passwordField.tap()
|
|
722
|
+
passwordField.typeText("password123")
|
|
723
|
+
|
|
724
|
+
loginButton.tap()
|
|
725
|
+
|
|
726
|
+
// Assert
|
|
727
|
+
XCTAssertTrue(app.navigationBars["Home"].waitForExistence(timeout: 5))
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### 6.3 Integration Testing
|
|
733
|
+
|
|
734
|
+
**Check for:**
|
|
735
|
+
- [ ] Repository + network integration tested
|
|
736
|
+
- [ ] Database operations tested
|
|
737
|
+
- [ ] End-to-end flows tested
|
|
738
|
+
- [ ] Real dependencies used (not mocks)
|
|
739
|
+
|
|
740
|
+
---
|
|
741
|
+
|
|
742
|
+
## 7. Code Organization
|
|
743
|
+
|
|
744
|
+
### 7.1 File Structure
|
|
745
|
+
|
|
746
|
+
**Check for:**
|
|
747
|
+
- [ ] Logical folder organization
|
|
748
|
+
- [ ] Feature-based grouping
|
|
749
|
+
- [ ] Clear separation of concerns
|
|
750
|
+
- [ ] Consistent naming
|
|
751
|
+
|
|
752
|
+
**Example Structure:**
|
|
753
|
+
```
|
|
754
|
+
MyApp/
|
|
755
|
+
├── App/
|
|
756
|
+
│ ├── MyApp.swift
|
|
757
|
+
│ └── AppDelegate.swift
|
|
758
|
+
├── Core/
|
|
759
|
+
│ ├── Network/
|
|
760
|
+
│ │ ├── NetworkClient.swift
|
|
761
|
+
│ │ └── APIEndpoint.swift
|
|
762
|
+
│ ├── Database/
|
|
763
|
+
│ │ └── Database.swift
|
|
764
|
+
│ └── DependencyInjection/
|
|
765
|
+
│ └── DependencyContainer.swift
|
|
766
|
+
├── Features/
|
|
767
|
+
│ ├── Login/
|
|
768
|
+
│ │ ├── Views/
|
|
769
|
+
│ │ │ ├── LoginView.swift
|
|
770
|
+
│ │ │ └── LoginFormView.swift
|
|
771
|
+
│ │ ├── ViewModels/
|
|
772
|
+
│ │ │ └── LoginViewModel.swift
|
|
773
|
+
│ │ ├── UseCases/
|
|
774
|
+
│ │ │ └── LoginUseCase.swift
|
|
775
|
+
│ │ └── Models/
|
|
776
|
+
│ │ └── LoginError.swift
|
|
777
|
+
│ ├── UserList/
|
|
778
|
+
│ │ ├── Views/
|
|
779
|
+
│ │ ├── ViewModels/
|
|
780
|
+
│ │ └── Models/
|
|
781
|
+
│ └── ...
|
|
782
|
+
├── Domain/
|
|
783
|
+
│ ├── Models/
|
|
784
|
+
│ │ └── User.swift
|
|
785
|
+
│ └── Repositories/
|
|
786
|
+
│ ├── UserRepository.swift
|
|
787
|
+
│ └── AuthRepository.swift
|
|
788
|
+
└── Resources/
|
|
789
|
+
├── Assets.xcassets
|
|
790
|
+
└── Localizable.strings
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### 7.2 MARK Comments
|
|
794
|
+
|
|
795
|
+
**Check for:**
|
|
796
|
+
- [ ] Consistent MARK usage
|
|
797
|
+
- [ ] Logical section ordering
|
|
798
|
+
- [ ] Protocol conformances in extensions
|
|
799
|
+
|
|
800
|
+
**Example:**
|
|
801
|
+
```swift
|
|
802
|
+
final class UserViewModel {
|
|
803
|
+
// MARK: - Properties
|
|
804
|
+
private let userRepository: UserRepository
|
|
805
|
+
@Published var users: [User] = []
|
|
806
|
+
|
|
807
|
+
// MARK: - Initialization
|
|
808
|
+
init(userRepository: UserRepository) {
|
|
809
|
+
self.userRepository = userRepository
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// MARK: - Public Methods
|
|
813
|
+
func loadUsers() async {
|
|
814
|
+
// Implementation
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// MARK: - Private Methods
|
|
818
|
+
private func processUsers(_ users: [User]) -> [User] {
|
|
819
|
+
// Implementation
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// MARK: - Equatable
|
|
824
|
+
extension UserViewModel: Equatable {
|
|
825
|
+
static func == (lhs: UserViewModel, rhs: UserViewModel) -> Bool {
|
|
826
|
+
lhs.users == rhs.users
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
---
|
|
832
|
+
|
|
833
|
+
## Quick Architecture Checklist
|
|
834
|
+
|
|
835
|
+
### Critical
|
|
836
|
+
- [ ] Clear separation of concerns (View/ViewModel/Model)
|
|
837
|
+
- [ ] Dependencies injected (no singletons or hard dependencies)
|
|
838
|
+
- [ ] Protocol-based abstractions
|
|
839
|
+
- [ ] Testable architecture
|
|
840
|
+
|
|
841
|
+
### High Priority
|
|
842
|
+
- [ ] Repository pattern for data access
|
|
843
|
+
- [ ] Use cases for business logic
|
|
844
|
+
- [ ] Coordinator for navigation
|
|
845
|
+
- [ ] Unit tests for ViewModels and use cases
|
|
846
|
+
|
|
847
|
+
### Medium Priority
|
|
848
|
+
- [ ] Consistent file organization
|
|
849
|
+
- [ ] MARK comments for sections
|
|
850
|
+
- [ ] Environment-based DI in SwiftUI
|
|
851
|
+
- [ ] Integration tests for critical paths
|
|
852
|
+
|
|
853
|
+
### Low Priority
|
|
854
|
+
- [ ] Dependency container
|
|
855
|
+
- [ ] Feature-based folder structure
|
|
856
|
+
- [ ] UI tests for user flows
|
|
857
|
+
|
|
858
|
+
---
|
|
859
|
+
|
|
860
|
+
## Version
|
|
861
|
+
**Last Updated**: 2026-02-10
|
|
862
|
+
**Version**: 1.0.0
|