@pilllesss/yorn 1.0.182 → 1.0.183
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.
Potentially problematic release.
This version of @pilllesss/yorn might be problematic. Click here for more details.
- package/README.md +1 -1
- package/dist/providers/data/.manifest.json +1 -1
- package/dist/skills/code-review/LICENSE +21 -0
- package/dist/skills/code-review/SKILL.md +233 -0
- package/dist/skills/code-review/assets/pr-review-template.md +137 -0
- package/dist/skills/code-review/assets/review-checklist.md +123 -0
- package/dist/skills/code-review/reference/angular.md +768 -0
- package/dist/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/dist/skills/code-review/reference/c.md +890 -0
- package/dist/skills/code-review/reference/code-quality-universal.md +488 -0
- package/dist/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/dist/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/dist/skills/code-review/reference/cpp.md +893 -0
- package/dist/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/dist/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/dist/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/dist/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/dist/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/dist/skills/code-review/reference/csharp.md +525 -0
- package/dist/skills/code-review/reference/css-less-sass.md +661 -0
- package/dist/skills/code-review/reference/dart.md +670 -0
- package/dist/skills/code-review/reference/django.md +985 -0
- package/dist/skills/code-review/reference/fastapi.md +580 -0
- package/dist/skills/code-review/reference/go.md +993 -0
- package/dist/skills/code-review/reference/java.md +409 -0
- package/dist/skills/code-review/reference/java8.md +586 -0
- package/dist/skills/code-review/reference/kotlin.md +1018 -0
- package/dist/skills/code-review/reference/nestjs.md +593 -0
- package/dist/skills/code-review/reference/performance-review-guide.md +816 -0
- package/dist/skills/code-review/reference/php.md +684 -0
- package/dist/skills/code-review/reference/python.md +1073 -0
- package/dist/skills/code-review/reference/qt.md +757 -0
- package/dist/skills/code-review/reference/react.md +871 -0
- package/dist/skills/code-review/reference/ruby.md +964 -0
- package/dist/skills/code-review/reference/rust.md +846 -0
- package/dist/skills/code-review/reference/security-review-guide.md +494 -0
- package/dist/skills/code-review/reference/svelte.md +1064 -0
- package/dist/skills/code-review/reference/swift.md +936 -0
- package/dist/skills/code-review/reference/typescript.md +1016 -0
- package/dist/skills/code-review/reference/vue.md +924 -0
- package/dist/skills/code-review/reference/zig.md +440 -0
- package/dist/skills/code-review/scripts/pr-analyzer.py +435 -0
- package/dist/skills/code-review/scripts/test_pr_analyzer.py +380 -0
- package/dist/yorn.cjs +628 -628
- package/package.json +2 -2
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
# Swift Code Review Guide
|
|
2
|
+
|
|
3
|
+
A code review checklist for modern Swift (5.9+/6), covering SwiftUI, Swift Concurrency, and the Swift API Design Guidelines.
|
|
4
|
+
|
|
5
|
+
## Quick Review Checklist
|
|
6
|
+
|
|
7
|
+
### Must-Check Items
|
|
8
|
+
- [ ] Are force-unwraps (`!`) and `try!` avoided in favor of safe unwrapping
|
|
9
|
+
- [ ] Do closures that capture `self` use `[weak self]` to avoid retain cycles
|
|
10
|
+
- [ ] Is the value vs reference type choice intentional (struct vs class)
|
|
11
|
+
- [ ] Are errors propagated with `throws`/`Result` instead of being swallowed
|
|
12
|
+
- [ ] Are concurrency boundaries data-race-safe (`Sendable`, `@MainActor`, actors)
|
|
13
|
+
|
|
14
|
+
### Common Issues
|
|
15
|
+
- [ ] Fire-and-forget `Task {}` that leaks or is never cancelled
|
|
16
|
+
- [ ] Wrong SwiftUI property wrapper (`@ObservedObject` where `@StateObject` is needed)
|
|
17
|
+
- [ ] O(n^2) lookups in loops that could use a `Set` or `Dictionary`
|
|
18
|
+
- [ ] Implicitly unwrapped optionals (`var x: T!`) outside of IBOutlets
|
|
19
|
+
- [ ] Over-broad access control (`public`/`open` where `internal` suffices)
|
|
20
|
+
- [ ] Naming that ignores the Swift API Design Guidelines
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## 1. Optionals and Unwrapping
|
|
25
|
+
|
|
26
|
+
### 1.1 Avoid Force-Unwrapping
|
|
27
|
+
|
|
28
|
+
```swift
|
|
29
|
+
// ❌ Wrong: crashes at runtime if nil
|
|
30
|
+
let name = user.name!
|
|
31
|
+
let url = URL(string: urlString)!
|
|
32
|
+
|
|
33
|
+
// ✅ Correct: bind with guard let / if let
|
|
34
|
+
guard let name = user.name else {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if let url = URL(string: urlString) {
|
|
39
|
+
load(url)
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### 1.2 Use Nil-Coalescing for Defaults
|
|
44
|
+
|
|
45
|
+
```swift
|
|
46
|
+
// ❌ Wrong: verbose and crash-prone
|
|
47
|
+
let count: Int
|
|
48
|
+
if let c = dictionary["count"] {
|
|
49
|
+
count = c
|
|
50
|
+
} else {
|
|
51
|
+
count = 0
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ✅ Correct: nil-coalescing
|
|
55
|
+
let count = dictionary["count"] ?? 0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 1.3 Prefer guard let for Early Exit
|
|
59
|
+
|
|
60
|
+
```swift
|
|
61
|
+
// ❌ Wrong: deep nesting (pyramid of doom)
|
|
62
|
+
func process(_ input: String?) {
|
|
63
|
+
if let input = input {
|
|
64
|
+
if let value = Int(input) {
|
|
65
|
+
if value > 0 {
|
|
66
|
+
handle(value)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ✅ Correct: guard keeps the happy path unindented
|
|
73
|
+
func process(_ input: String?) {
|
|
74
|
+
guard let input,
|
|
75
|
+
let value = Int(input),
|
|
76
|
+
value > 0 else {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
handle(value)
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 1.4 Avoid Implicitly Unwrapped Optionals
|
|
84
|
+
|
|
85
|
+
```swift
|
|
86
|
+
// ❌ Wrong: T! is a hidden force-unwrap on every access
|
|
87
|
+
class ViewModel {
|
|
88
|
+
var service: NetworkService!
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ✅ Correct: inject a non-optional dependency
|
|
92
|
+
class ViewModel {
|
|
93
|
+
private let service: NetworkService
|
|
94
|
+
|
|
95
|
+
init(service: NetworkService) {
|
|
96
|
+
self.service = service
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 1.5 Use Optional Chaining and map/flatMap
|
|
102
|
+
|
|
103
|
+
```swift
|
|
104
|
+
// ❌ Wrong: manual unwrapping just to transform
|
|
105
|
+
var initial: String?
|
|
106
|
+
if let name = user.name {
|
|
107
|
+
initial = String(name.prefix(1))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ✅ Correct: optional chaining + map
|
|
111
|
+
let initial = user.name.map { String($0.prefix(1)) }
|
|
112
|
+
|
|
113
|
+
// ✅ Correct: flatMap to avoid double optionals
|
|
114
|
+
let port: Int? = components.port.flatMap { Int(exactly: $0) }
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 2. Memory Management and Retain Cycles
|
|
120
|
+
|
|
121
|
+
### 2.1 Use [weak self] in Escaping Closures
|
|
122
|
+
|
|
123
|
+
```swift
|
|
124
|
+
// ❌ Wrong: closure strongly captures self, creating a retain cycle
|
|
125
|
+
class ImageLoader {
|
|
126
|
+
var onComplete: (() -> Void)?
|
|
127
|
+
|
|
128
|
+
func load() {
|
|
129
|
+
service.fetch { data in
|
|
130
|
+
self.cache = data // self is retained by the closure
|
|
131
|
+
self.onComplete?()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ✅ Correct: capture self weakly and guard
|
|
137
|
+
class ImageLoader {
|
|
138
|
+
var onComplete: (() -> Void)?
|
|
139
|
+
|
|
140
|
+
func load() {
|
|
141
|
+
service.fetch { [weak self] data in
|
|
142
|
+
guard let self else { return }
|
|
143
|
+
self.cache = data
|
|
144
|
+
self.onComplete?()
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 2.2 weak vs unowned
|
|
151
|
+
|
|
152
|
+
```swift
|
|
153
|
+
// ✅ Use weak when the reference can legitimately become nil
|
|
154
|
+
class Controller {
|
|
155
|
+
weak var delegate: ControllerDelegate?
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ✅ Use unowned only when the captured object is guaranteed to
|
|
159
|
+
// outlive the closure (e.g. self owns the closure tightly).
|
|
160
|
+
// unowned crashes if accessed after deallocation.
|
|
161
|
+
class Owner {
|
|
162
|
+
lazy var describe: () -> String = { [unowned self] in
|
|
163
|
+
self.name
|
|
164
|
+
}
|
|
165
|
+
let name = "owner"
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ❌ Wrong: unowned on something that can outlive self -> crash
|
|
169
|
+
networkClient.onResponse = { [unowned self] in self.update() }
|
|
170
|
+
// Prefer [weak self] here, since onResponse may fire after self is gone.
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### 2.3 Break Delegate Retain Cycles
|
|
174
|
+
|
|
175
|
+
```swift
|
|
176
|
+
// ❌ Wrong: strong delegate keeps both objects alive forever
|
|
177
|
+
protocol DataSourceDelegate: AnyObject {}
|
|
178
|
+
|
|
179
|
+
class DataSource {
|
|
180
|
+
var delegate: DataSourceDelegate? // strong by default
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ✅ Correct: delegates should be weak (and protocol AnyObject-bound)
|
|
184
|
+
class DataSource {
|
|
185
|
+
weak var delegate: DataSourceDelegate?
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### 2.4 Closures Stored as Properties
|
|
190
|
+
|
|
191
|
+
```swift
|
|
192
|
+
// ❌ Wrong: stored closure captures self strongly -> permanent cycle
|
|
193
|
+
class Timer {
|
|
194
|
+
var tick: (() -> Void)!
|
|
195
|
+
func configure() {
|
|
196
|
+
tick = { self.count += 1 }
|
|
197
|
+
}
|
|
198
|
+
var count = 0
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ✅ Correct: weak capture for stored closures referencing self
|
|
202
|
+
class Timer {
|
|
203
|
+
var tick: (() -> Void)?
|
|
204
|
+
func configure() {
|
|
205
|
+
tick = { [weak self] in self?.count += 1 }
|
|
206
|
+
}
|
|
207
|
+
var count = 0
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## 3. Value vs Reference Types
|
|
214
|
+
|
|
215
|
+
### 3.1 Prefer Structs by Default
|
|
216
|
+
|
|
217
|
+
```swift
|
|
218
|
+
// ✅ Use a struct for data/models with value semantics
|
|
219
|
+
struct Coordinate {
|
|
220
|
+
var latitude: Double
|
|
221
|
+
var longitude: Double
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Copies are independent; no shared mutable state, thread-friendly.
|
|
225
|
+
var a = Coordinate(latitude: 1, longitude: 2)
|
|
226
|
+
var b = a
|
|
227
|
+
b.latitude = 99 // a is unchanged
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### 3.2 Use a Class for Identity or Shared State
|
|
231
|
+
|
|
232
|
+
```swift
|
|
233
|
+
// ✅ Use a class when instances have identity or must be shared/mutated
|
|
234
|
+
// by reference, or when you need inheritance / Objective-C interop.
|
|
235
|
+
final class DatabaseConnection {
|
|
236
|
+
private(set) var isOpen = false
|
|
237
|
+
func open() { isOpen = true }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Two references point to the same connection.
|
|
241
|
+
let conn1 = DatabaseConnection()
|
|
242
|
+
let conn2 = conn1
|
|
243
|
+
conn1.open()
|
|
244
|
+
// conn2.isOpen == true
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### 3.3 Mark Classes final When Not Subclassed
|
|
248
|
+
|
|
249
|
+
```swift
|
|
250
|
+
// ❌ Wrong: open to subclassing unintentionally (slower dispatch, fragile API)
|
|
251
|
+
class UserViewModel {}
|
|
252
|
+
|
|
253
|
+
// ✅ Correct: final enables static dispatch and signals intent
|
|
254
|
+
final class UserViewModel {}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 3.4 Beware Reference Types Inside Structs
|
|
258
|
+
|
|
259
|
+
```swift
|
|
260
|
+
// ❌ Surprising: struct copy still shares the inner class instance
|
|
261
|
+
final class Box { var value = 0 }
|
|
262
|
+
struct Container { var box = Box() }
|
|
263
|
+
|
|
264
|
+
var x = Container()
|
|
265
|
+
var y = x
|
|
266
|
+
y.box.value = 42 // x.box.value is also 42 (shared reference!)
|
|
267
|
+
|
|
268
|
+
// ✅ Correct: use value semantics throughout, or copy on write deliberately
|
|
269
|
+
struct Container {
|
|
270
|
+
var value = 0 // plain value type, copies are independent
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## 4. Error Handling
|
|
277
|
+
|
|
278
|
+
> 📖 For cross-language error handling principles, see [Error Handling Guide](cross-cutting/error-handling-principles.md)
|
|
279
|
+
|
|
280
|
+
### 4.1 Avoid try! and try?
|
|
281
|
+
|
|
282
|
+
```swift
|
|
283
|
+
// ❌ Wrong: try! crashes on any thrown error
|
|
284
|
+
let data = try! Data(contentsOf: url)
|
|
285
|
+
|
|
286
|
+
// ❌ Often wrong: try? silently discards the error and the cause
|
|
287
|
+
let data = try? Data(contentsOf: url) // data is nil, you lose "why"
|
|
288
|
+
|
|
289
|
+
// ✅ Correct: propagate or handle with do-catch
|
|
290
|
+
do {
|
|
291
|
+
let data = try Data(contentsOf: url)
|
|
292
|
+
process(data)
|
|
293
|
+
} catch {
|
|
294
|
+
log.error("failed to read \(url): \(error)")
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### 4.2 Define Meaningful Error Types
|
|
299
|
+
|
|
300
|
+
```swift
|
|
301
|
+
// ✅ Recommended: an Error enum communicates failure modes precisely
|
|
302
|
+
enum NetworkError: Error {
|
|
303
|
+
case invalidURL
|
|
304
|
+
case unauthorized
|
|
305
|
+
case server(statusCode: Int)
|
|
306
|
+
case decoding(underlying: Error)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
func fetch(_ path: String) throws -> Data {
|
|
310
|
+
guard let url = URL(string: path) else {
|
|
311
|
+
throw NetworkError.invalidURL
|
|
312
|
+
}
|
|
313
|
+
// ...
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### 4.3 Use Result for Stored or Deferred Outcomes
|
|
318
|
+
|
|
319
|
+
```swift
|
|
320
|
+
// ✅ Result is useful at callback boundaries or when storing an outcome
|
|
321
|
+
func load(completion: @escaping (Result<User, NetworkError>) -> Void) {
|
|
322
|
+
// completion(.success(user)) or completion(.failure(.unauthorized))
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// ✅ Convert between Result and throws as needed
|
|
326
|
+
let user = try result.get()
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
### 4.4 Typed Throws (Swift 6)
|
|
330
|
+
|
|
331
|
+
```swift
|
|
332
|
+
// ✅ Typed throws constrains the error type when it is fully known.
|
|
333
|
+
// Use it for closed, exhaustive error domains; prefer untyped
|
|
334
|
+
// `throws` for library APIs that may grow new error cases.
|
|
335
|
+
func parse(_ raw: String) throws(ParsingError) -> Token {
|
|
336
|
+
guard let token = Token(raw) else {
|
|
337
|
+
throw ParsingError.malformed
|
|
338
|
+
}
|
|
339
|
+
return token
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
do {
|
|
343
|
+
let token = try parse(input)
|
|
344
|
+
} catch {
|
|
345
|
+
// `error` is statically known to be ParsingError
|
|
346
|
+
handle(error)
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### 4.5 Don't Catch and Rethrow Without Value
|
|
351
|
+
|
|
352
|
+
```swift
|
|
353
|
+
// ❌ Wrong: catch that adds nothing but obscures the trace
|
|
354
|
+
do {
|
|
355
|
+
try work()
|
|
356
|
+
} catch {
|
|
357
|
+
throw error // pointless
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// ✅ Correct: only catch to add context or recover
|
|
361
|
+
do {
|
|
362
|
+
try work()
|
|
363
|
+
} catch {
|
|
364
|
+
throw AppError.workFailed(underlying: error)
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
## 5. Swift Concurrency
|
|
371
|
+
|
|
372
|
+
> 📖 For cross-language concurrency patterns, see [Async & Concurrency Guide](cross-cutting/async-concurrency-patterns.md)
|
|
373
|
+
|
|
374
|
+
### 5.1 Prefer async/await Over Nested Callbacks
|
|
375
|
+
|
|
376
|
+
```swift
|
|
377
|
+
// ❌ Wrong: callback pyramid, error handling scattered
|
|
378
|
+
func loadProfile(completion: @escaping (Result<Profile, Error>) -> Void) {
|
|
379
|
+
fetchUser { userResult in
|
|
380
|
+
switch userResult {
|
|
381
|
+
case .success(let user):
|
|
382
|
+
fetchAvatar(user) { avatarResult in /* ... */ }
|
|
383
|
+
case .failure(let error):
|
|
384
|
+
completion(.failure(error))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ✅ Correct: linear async/await
|
|
390
|
+
func loadProfile() async throws -> Profile {
|
|
391
|
+
let user = try await fetchUser()
|
|
392
|
+
let avatar = try await fetchAvatar(user)
|
|
393
|
+
return Profile(user: user, avatar: avatar)
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### 5.2 Use @MainActor for UI State
|
|
398
|
+
|
|
399
|
+
```swift
|
|
400
|
+
// ❌ Wrong: mutating UI state from a background context (data race / crash)
|
|
401
|
+
func refresh() async {
|
|
402
|
+
let items = try? await api.load()
|
|
403
|
+
self.items = items ?? [] // may run off the main thread
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ✅ Correct: isolate UI-facing types to the main actor
|
|
407
|
+
@MainActor
|
|
408
|
+
final class FeedViewModel: ObservableObject {
|
|
409
|
+
@Published var items: [Item] = []
|
|
410
|
+
|
|
411
|
+
func refresh() async {
|
|
412
|
+
let loaded = (try? await api.load()) ?? []
|
|
413
|
+
items = loaded // guaranteed on the main actor
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
### 5.3 Protect Mutable State with Actors
|
|
419
|
+
|
|
420
|
+
```swift
|
|
421
|
+
// ❌ Wrong: shared mutable state without synchronization (data race)
|
|
422
|
+
final class Counter {
|
|
423
|
+
var value = 0
|
|
424
|
+
func increment() { value += 1 }
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ✅ Correct: an actor serializes access to its mutable state
|
|
428
|
+
actor Counter {
|
|
429
|
+
private(set) var value = 0
|
|
430
|
+
func increment() { value += 1 }
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
let counter = Counter()
|
|
434
|
+
await counter.increment() // access is awaited and serialized
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### 5.4 Conform Shared Types to Sendable
|
|
438
|
+
|
|
439
|
+
```swift
|
|
440
|
+
// ❌ Wrong: passing a non-Sendable class across actors (Swift 6 error)
|
|
441
|
+
final class Config { // mutable, not Sendable
|
|
442
|
+
var retries = 3
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ✅ Correct: make shared types Sendable (immutable value type is ideal)
|
|
446
|
+
struct Config: Sendable {
|
|
447
|
+
let retries: Int
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ✅ For reference types, use final + immutable stored properties,
|
|
451
|
+
// or @unchecked Sendable only with manual synchronization.
|
|
452
|
+
final class Cache: @unchecked Sendable {
|
|
453
|
+
private let lock = NSLock()
|
|
454
|
+
private var storage: [String: Data] = [:]
|
|
455
|
+
// all access guarded by lock
|
|
456
|
+
}
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
### 5.5 Handle Task Cancellation
|
|
460
|
+
|
|
461
|
+
```swift
|
|
462
|
+
// ❌ Wrong: ignores cancellation, keeps working after the view is gone
|
|
463
|
+
func search(_ query: String) async -> [Result] {
|
|
464
|
+
var results: [Result] = []
|
|
465
|
+
for page in 0..<100 {
|
|
466
|
+
results += await fetchPage(query, page) // never stops
|
|
467
|
+
}
|
|
468
|
+
return results
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// ✅ Correct: check for cancellation cooperatively
|
|
472
|
+
func search(_ query: String) async throws -> [Result] {
|
|
473
|
+
var results: [Result] = []
|
|
474
|
+
for page in 0..<100 {
|
|
475
|
+
try Task.checkCancellation()
|
|
476
|
+
results += try await fetchPage(query, page)
|
|
477
|
+
}
|
|
478
|
+
return results
|
|
479
|
+
}
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### 5.6 Don't Leak Fire-and-Forget Tasks
|
|
483
|
+
|
|
484
|
+
```swift
|
|
485
|
+
// ❌ Wrong: unstructured Task with no handle, never cancelled
|
|
486
|
+
final class ViewModel {
|
|
487
|
+
func onAppear() {
|
|
488
|
+
Task {
|
|
489
|
+
await self.stream() // runs forever even after dismissal
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ✅ Correct: retain the handle and cancel it (or use .task in SwiftUI)
|
|
495
|
+
final class ViewModel {
|
|
496
|
+
private var streamTask: Task<Void, Never>?
|
|
497
|
+
|
|
498
|
+
func onAppear() {
|
|
499
|
+
streamTask = Task { [weak self] in
|
|
500
|
+
await self?.stream()
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
func onDisappear() {
|
|
505
|
+
streamTask?.cancel()
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### 5.7 Use Structured Concurrency for Parallelism
|
|
511
|
+
|
|
512
|
+
```swift
|
|
513
|
+
// ❌ Wrong: sequential awaits where work could run concurrently
|
|
514
|
+
let a = await loadA()
|
|
515
|
+
let b = await loadB() // waits for A to finish first
|
|
516
|
+
|
|
517
|
+
// ✅ Correct: async let runs them concurrently
|
|
518
|
+
async let a = loadA()
|
|
519
|
+
async let b = loadB()
|
|
520
|
+
let (resultA, resultB) = await (a, b)
|
|
521
|
+
|
|
522
|
+
// ✅ For a dynamic number of children, use a task group
|
|
523
|
+
try await withThrowingTaskGroup(of: Item.self) { group in
|
|
524
|
+
for id in ids {
|
|
525
|
+
group.addTask { try await fetch(id) }
|
|
526
|
+
}
|
|
527
|
+
for try await item in group {
|
|
528
|
+
store(item)
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## 6. SwiftUI
|
|
536
|
+
|
|
537
|
+
### 6.1 Choose the Right State Wrapper
|
|
538
|
+
|
|
539
|
+
```swift
|
|
540
|
+
// ✅ @State: simple value-type state owned by this view
|
|
541
|
+
struct Toggle: View {
|
|
542
|
+
@State private var isOn = false
|
|
543
|
+
var body: some View { /* ... */ }
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ✅ @StateObject: the view CREATES and OWNS a reference-type model
|
|
547
|
+
struct ProfileScreen: View {
|
|
548
|
+
@StateObject private var model = ProfileViewModel()
|
|
549
|
+
var body: some View { /* ... */ }
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// ✅ @ObservedObject: the model is OWNED elsewhere and passed in
|
|
553
|
+
struct ProfileHeader: View {
|
|
554
|
+
@ObservedObject var model: ProfileViewModel
|
|
555
|
+
var body: some View { /* ... */ }
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// ✅ @Binding: a two-way reference to state owned by a parent
|
|
559
|
+
struct SearchField: View {
|
|
560
|
+
@Binding var text: String
|
|
561
|
+
var body: some View { /* ... */ }
|
|
562
|
+
}
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### 6.2 @StateObject vs @ObservedObject
|
|
566
|
+
|
|
567
|
+
```swift
|
|
568
|
+
// ❌ Wrong: @ObservedObject for an object the view itself creates.
|
|
569
|
+
// SwiftUI may recreate the view, re-instantiating the model and
|
|
570
|
+
// losing its state on every re-render.
|
|
571
|
+
struct CounterView: View {
|
|
572
|
+
@ObservedObject var model = CounterModel() // recreated unexpectedly
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ✅ Correct: @StateObject ties the model's lifetime to the view
|
|
576
|
+
struct CounterView: View {
|
|
577
|
+
@StateObject private var model = CounterModel()
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### 6.3 Preserve View Identity
|
|
582
|
+
|
|
583
|
+
```swift
|
|
584
|
+
// ❌ Wrong: index-based id reuses identity when the array reorders,
|
|
585
|
+
// causing wrong animations and stale state.
|
|
586
|
+
ForEach(0..<items.count, id: \.self) { i in
|
|
587
|
+
ItemRow(item: items[i])
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ✅ Correct: use a stable, unique identifier
|
|
591
|
+
ForEach(items) { item in // Item: Identifiable
|
|
592
|
+
ItemRow(item: item)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ✅ Use .id(...) to deliberately reset a view's state
|
|
596
|
+
ProfileView(user: user)
|
|
597
|
+
.id(user.id) // new identity per user -> fresh state
|
|
598
|
+
```
|
|
599
|
+
|
|
600
|
+
### 6.4 Avoid Over-Rendering
|
|
601
|
+
|
|
602
|
+
```swift
|
|
603
|
+
// ❌ Wrong: a single huge body re-renders everything on any change
|
|
604
|
+
struct Dashboard: View {
|
|
605
|
+
@ObservedObject var model: DashboardModel
|
|
606
|
+
var body: some View {
|
|
607
|
+
VStack {
|
|
608
|
+
// header + heavy chart + list all recompute together
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ✅ Correct: extract subviews so only the affected part re-renders.
|
|
614
|
+
// Each child observes only the state it needs.
|
|
615
|
+
struct Dashboard: View {
|
|
616
|
+
var body: some View {
|
|
617
|
+
VStack {
|
|
618
|
+
HeaderView()
|
|
619
|
+
ChartView()
|
|
620
|
+
ItemList()
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
### 6.5 Do Async Work with .task
|
|
627
|
+
|
|
628
|
+
```swift
|
|
629
|
+
// ❌ Wrong: kicking off work in onAppear without cancellation
|
|
630
|
+
.onAppear {
|
|
631
|
+
Task { await model.load() } // not cancelled when view disappears
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// ✅ Correct: .task is tied to the view's lifetime and auto-cancels
|
|
635
|
+
.task {
|
|
636
|
+
await model.load()
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// ✅ Re-run when an input changes
|
|
640
|
+
.task(id: query) {
|
|
641
|
+
await model.search(query)
|
|
642
|
+
}
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## 7. Protocols and Generics
|
|
648
|
+
|
|
649
|
+
### 7.1 Protocol-Oriented Design
|
|
650
|
+
|
|
651
|
+
```swift
|
|
652
|
+
// ✅ Compose behavior with protocols and default implementations
|
|
653
|
+
protocol Identifiable2 {
|
|
654
|
+
var id: String { get }
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
protocol Describable {
|
|
658
|
+
var description: String { get }
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
extension Describable {
|
|
662
|
+
var description: String { "no description" } // default
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### 7.2 Prefer some Over any
|
|
667
|
+
|
|
668
|
+
```swift
|
|
669
|
+
// ❌ Slower: `any` is an existential box with dynamic dispatch
|
|
670
|
+
func makeShape() -> any Shape { Circle() }
|
|
671
|
+
|
|
672
|
+
// ✅ Faster: `some` is an opaque type resolved at compile time,
|
|
673
|
+
// preserving the concrete type and enabling static dispatch.
|
|
674
|
+
func makeShape() -> some Shape { Circle() }
|
|
675
|
+
|
|
676
|
+
// Use `any` only when you genuinely need heterogeneous values:
|
|
677
|
+
let shapes: [any Shape] = [Circle(), Square()]
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
### 7.3 Generic Constraints Over Existentials
|
|
681
|
+
|
|
682
|
+
```swift
|
|
683
|
+
// ❌ Wrong: existential parameter loses the concrete type and is slower
|
|
684
|
+
func logTotal(_ items: [any Numeric]) {
|
|
685
|
+
// awkward: the concrete numeric type is erased, so arithmetic needs casts
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// ✅ Correct: a generic constraint keeps full type information
|
|
689
|
+
func total<T: Numeric>(_ items: [T]) -> T {
|
|
690
|
+
items.reduce(.zero, +)
|
|
691
|
+
}
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
### 7.4 Associated Types with Primary Associated Types
|
|
695
|
+
|
|
696
|
+
```swift
|
|
697
|
+
// ✅ Primary associated types (Swift 5.7+) allow lightweight constraints
|
|
698
|
+
protocol Container<Item> {
|
|
699
|
+
associatedtype Item
|
|
700
|
+
var count: Int { get }
|
|
701
|
+
subscript(_ index: Int) -> Item { get }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Constrain the element type without a where-clause:
|
|
705
|
+
func first(in container: some Container<Int>) -> Int {
|
|
706
|
+
container[0]
|
|
707
|
+
}
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
---
|
|
711
|
+
|
|
712
|
+
## 8. Access Control and API Design
|
|
713
|
+
|
|
714
|
+
### 8.1 Use the Narrowest Access Level
|
|
715
|
+
|
|
716
|
+
```swift
|
|
717
|
+
// ❌ Wrong: everything public exposes internal details as API surface
|
|
718
|
+
public class Service {
|
|
719
|
+
public var cache: [String: Data] = [:]
|
|
720
|
+
public func reset() {}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// ✅ Correct: expose only the intended API; hide the rest
|
|
724
|
+
public final class Service {
|
|
725
|
+
private var cache: [String: Data] = [:]
|
|
726
|
+
public func reset() { cache.removeAll() }
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
### 8.2 private vs fileprivate vs internal vs public/open
|
|
731
|
+
|
|
732
|
+
```swift
|
|
733
|
+
// private: visible only within the enclosing declaration (and its extensions in the same file)
|
|
734
|
+
// fileprivate: visible within the same source file
|
|
735
|
+
// internal: visible within the module (the default)
|
|
736
|
+
// public: visible outside the module, but not subclassable/overridable
|
|
737
|
+
// open: visible outside the module AND subclassable/overridable
|
|
738
|
+
|
|
739
|
+
// ✅ Use private(set) to expose read-only state
|
|
740
|
+
public final class Account {
|
|
741
|
+
public private(set) var balance: Decimal = 0
|
|
742
|
+
}
|
|
743
|
+
```
|
|
744
|
+
|
|
745
|
+
### 8.3 Follow the Swift API Design Guidelines
|
|
746
|
+
|
|
747
|
+
```swift
|
|
748
|
+
// ❌ Wrong: redundant words, unclear argument roles
|
|
749
|
+
func insertObject(_ object: Element, atIndex index: Int)
|
|
750
|
+
list.removeElement(at: 0)
|
|
751
|
+
|
|
752
|
+
// ✅ Correct: read at the call site like a phrase; omit needless words
|
|
753
|
+
func insert(_ element: Element, at index: Int)
|
|
754
|
+
list.insert(item, at: 0) // reads as "insert item at 0"
|
|
755
|
+
list.remove(at: 0)
|
|
756
|
+
|
|
757
|
+
// ✅ Boolean properties read as assertions
|
|
758
|
+
var isEmpty: Bool
|
|
759
|
+
var hasChanges: Bool
|
|
760
|
+
```
|
|
761
|
+
|
|
762
|
+
### 8.4 Name Methods by Side Effects
|
|
763
|
+
|
|
764
|
+
```swift
|
|
765
|
+
// ✅ Mutating verb vs non-mutating noun pairs (the "ed/ing" rule)
|
|
766
|
+
var sorted = array.sorted() // returns a new value (non-mutating)
|
|
767
|
+
array.sort() // mutates in place (imperative verb)
|
|
768
|
+
|
|
769
|
+
let reversed = text.reversed()
|
|
770
|
+
text.reverse()
|
|
771
|
+
```
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## 9. Collections and Functional Style
|
|
776
|
+
|
|
777
|
+
### 9.1 Prefer map/filter/compactMap
|
|
778
|
+
|
|
779
|
+
```swift
|
|
780
|
+
// ❌ Verbose: manual loop with mutable accumulator
|
|
781
|
+
var names: [String] = []
|
|
782
|
+
for user in users {
|
|
783
|
+
if user.isActive {
|
|
784
|
+
names.append(user.name)
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// ✅ Correct: declarative transform
|
|
789
|
+
let names = users.filter(\.isActive).map(\.name)
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
### 9.2 compactMap to Drop nils
|
|
793
|
+
|
|
794
|
+
```swift
|
|
795
|
+
// ❌ Wrong: map leaves an [Int?] you then have to unwrap
|
|
796
|
+
let numbers = strings.map { Int($0) } // [Int?]
|
|
797
|
+
|
|
798
|
+
// ✅ Correct: compactMap removes nils and unwraps
|
|
799
|
+
let numbers = strings.compactMap { Int($0) } // [Int]
|
|
800
|
+
```
|
|
801
|
+
|
|
802
|
+
### 9.3 Avoid O(n^2) Membership Checks
|
|
803
|
+
|
|
804
|
+
```swift
|
|
805
|
+
// ❌ Wrong: contains on an Array is O(n); the loop is O(n*m)
|
|
806
|
+
let result = candidates.filter { blocked.contains($0) } // blocked: [ID]
|
|
807
|
+
|
|
808
|
+
// ✅ Correct: a Set makes membership O(1)
|
|
809
|
+
let blockedSet = Set(blocked)
|
|
810
|
+
let result = candidates.filter { blockedSet.contains($0) }
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
### 9.4 reduce and Dictionary Grouping
|
|
814
|
+
|
|
815
|
+
```swift
|
|
816
|
+
// ✅ Group with Dictionary(grouping:)
|
|
817
|
+
let byFirstLetter = Dictionary(grouping: words) { $0.first }
|
|
818
|
+
|
|
819
|
+
// ❌ Wrong: reduce(into:) is preferred over reduce that copies each step
|
|
820
|
+
let total = numbers.reduce(0) { $0 + $1 } // fine for scalars
|
|
821
|
+
|
|
822
|
+
// ✅ Use reduce(into:) when accumulating into a collection (avoids copies)
|
|
823
|
+
let counts = words.reduce(into: [:]) { acc, word in
|
|
824
|
+
acc[word, default: 0] += 1
|
|
825
|
+
}
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### 9.5 Use lazy for Chained Transforms on Large Sequences
|
|
829
|
+
|
|
830
|
+
```swift
|
|
831
|
+
// ❌ Wrong: each step allocates an intermediate array
|
|
832
|
+
let firstMatch = bigArray.map(expensive).filter(isValid).first
|
|
833
|
+
|
|
834
|
+
// ✅ Correct: lazy avoids intermediate arrays and stops early
|
|
835
|
+
let firstMatch = bigArray.lazy.map(expensive).filter(isValid).first
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## 10. Testing
|
|
841
|
+
|
|
842
|
+
### 10.1 Arrange-Act-Assert with XCTest
|
|
843
|
+
|
|
844
|
+
```swift
|
|
845
|
+
import XCTest
|
|
846
|
+
@testable import MyApp
|
|
847
|
+
|
|
848
|
+
final class PriceCalculatorTests: XCTestCase {
|
|
849
|
+
func testDiscountApplied() {
|
|
850
|
+
// Arrange
|
|
851
|
+
let calculator = PriceCalculator(discount: 0.1)
|
|
852
|
+
// Act
|
|
853
|
+
let total = calculator.total(for: 100)
|
|
854
|
+
// Assert
|
|
855
|
+
XCTAssertEqual(total, 90, accuracy: 0.001)
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### 10.2 Testing async Code
|
|
861
|
+
|
|
862
|
+
```swift
|
|
863
|
+
// ✅ Mark the test method async and await directly
|
|
864
|
+
func testFetchUser() async throws {
|
|
865
|
+
let service = UserService(client: MockClient())
|
|
866
|
+
let user = try await service.fetchUser(id: "42")
|
|
867
|
+
XCTAssertEqual(user.id, "42")
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// ✅ Assert that an async call throws the expected error
|
|
871
|
+
func testFetchUserUnauthorized() async {
|
|
872
|
+
let service = UserService(client: UnauthorizedClient())
|
|
873
|
+
do {
|
|
874
|
+
_ = try await service.fetchUser(id: "42")
|
|
875
|
+
XCTFail("expected to throw")
|
|
876
|
+
} catch NetworkError.unauthorized {
|
|
877
|
+
// expected
|
|
878
|
+
} catch {
|
|
879
|
+
XCTFail("unexpected error: \(error)")
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
### 10.3 Inject Dependencies via Protocols
|
|
885
|
+
|
|
886
|
+
```swift
|
|
887
|
+
// ✅ Depend on a protocol so tests can substitute a mock
|
|
888
|
+
protocol HTTPClient {
|
|
889
|
+
func get(_ url: URL) async throws -> Data
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
struct MockClient: HTTPClient {
|
|
893
|
+
var result: Result<Data, Error>
|
|
894
|
+
func get(_ url: URL) async throws -> Data {
|
|
895
|
+
try result.get()
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
### 10.4 Avoid Sleeps; Await Expectations or Values
|
|
901
|
+
|
|
902
|
+
```swift
|
|
903
|
+
// ❌ Wrong: arbitrary sleep makes tests slow and flaky
|
|
904
|
+
func testCallback() {
|
|
905
|
+
var done = false
|
|
906
|
+
object.run { done = true }
|
|
907
|
+
Thread.sleep(forTimeInterval: 1)
|
|
908
|
+
XCTAssertTrue(done)
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// ✅ Correct: use XCTestExpectation for callback APIs
|
|
912
|
+
func testCallback() {
|
|
913
|
+
let expectation = expectation(description: "callback fired")
|
|
914
|
+
object.run { expectation.fulfill() }
|
|
915
|
+
wait(for: [expectation], timeout: 1.0)
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// ✅ Better: refactor to async and await the value directly
|
|
919
|
+
func testCallback() async {
|
|
920
|
+
let value = await object.run()
|
|
921
|
+
XCTAssertEqual(value, expected)
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
---
|
|
926
|
+
|
|
927
|
+
## References
|
|
928
|
+
|
|
929
|
+
- [Swift API Design Guidelines](https://www.swift.org/documentation/api-design-guidelines/)
|
|
930
|
+
- [The Swift Programming Language](https://docs.swift.org/swift-book/)
|
|
931
|
+
- [Swift Concurrency (TSPL)](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)
|
|
932
|
+
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/)
|
|
933
|
+
- [Apple: Managing Model Data in Your App (SwiftUI)](https://developer.apple.com/documentation/swiftui/managing-model-data-in-your-app)
|
|
934
|
+
- [Apple: Automatic Reference Counting](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/automaticreferencecounting/)
|
|
935
|
+
- [WWDC: Protocol-Oriented Programming in Swift](https://developer.apple.com/videos/play/wwdc2015/408/)
|
|
936
|
+
- [Swift Evolution](https://github.com/apple/swift-evolution)
|