@aicgen/aicgen 1.0.0-beta.2 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/rules/api-design.md +649 -0
- package/.agent/rules/architecture.md +2507 -0
- package/.agent/rules/best-practices.md +622 -0
- package/.agent/rules/code-style.md +308 -0
- package/.agent/rules/design-patterns.md +577 -0
- package/.agent/rules/devops.md +230 -0
- package/.agent/rules/error-handling.md +417 -0
- package/.agent/rules/instructions.md +28 -0
- package/.agent/rules/language.md +786 -0
- package/.agent/rules/performance.md +710 -0
- package/.agent/rules/security.md +587 -0
- package/.agent/rules/testing.md +572 -0
- package/.agent/workflows/add-documentation.md +10 -0
- package/.agent/workflows/generate-integration-tests.md +10 -0
- package/.agent/workflows/generate-unit-tests.md +11 -0
- package/.agent/workflows/performance-audit.md +11 -0
- package/.agent/workflows/refactor-extract-module.md +12 -0
- package/.agent/workflows/security-audit.md +12 -0
- package/.gemini/instructions.md +4843 -0
- package/AGENTS.md +9 -11
- package/bun.lock +755 -4
- package/claude.md +2 -2
- package/config.example.yml +129 -0
- package/config.yml +38 -0
- package/data/guideline-mappings.yml +128 -0
- package/data/language/dart/async.md +289 -0
- package/data/language/dart/basics.md +280 -0
- package/data/language/dart/error-handling.md +355 -0
- package/data/language/dart/index.md +10 -0
- package/data/language/dart/testing.md +352 -0
- package/data/language/swift/basics.md +477 -0
- package/data/language/swift/concurrency.md +654 -0
- package/data/language/swift/error-handling.md +679 -0
- package/data/language/swift/swiftui-mvvm.md +795 -0
- package/data/language/swift/testing.md +708 -0
- package/data/version.json +10 -8
- package/dist/index.js +50295 -29101
- package/jest.config.js +46 -0
- package/package.json +13 -2
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
# Swift Concurrency
|
|
2
|
+
|
|
3
|
+
## Async/Await Fundamentals
|
|
4
|
+
|
|
5
|
+
Swift's modern concurrency model uses async/await for asynchronous operations:
|
|
6
|
+
|
|
7
|
+
```swift
|
|
8
|
+
// ✅ Async function definition
|
|
9
|
+
func fetchUser(id: String) async throws -> User {
|
|
10
|
+
let url = URL(string: "https://api.example.com/users/\(id)")!
|
|
11
|
+
let (data, _) = try await URLSession.shared.data(from: url)
|
|
12
|
+
return try JSONDecoder().decode(User.self, from: data)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// ✅ Calling async functions
|
|
16
|
+
func loadUserData() async {
|
|
17
|
+
do {
|
|
18
|
+
let user = try await fetchUser(id: "123")
|
|
19
|
+
print("User: \(user.name)")
|
|
20
|
+
} catch {
|
|
21
|
+
print("Error: \(error)")
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ❌ Cannot call async function without await
|
|
26
|
+
func badExample() {
|
|
27
|
+
let user = fetchUser(id: "123") // Compile error
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Actors for Thread Safety
|
|
32
|
+
|
|
33
|
+
Actors ensure data race safety by serializing access to mutable state:
|
|
34
|
+
|
|
35
|
+
```swift
|
|
36
|
+
// ✅ Actor protects mutable state
|
|
37
|
+
actor Counter {
|
|
38
|
+
private var value = 0
|
|
39
|
+
|
|
40
|
+
func increment() {
|
|
41
|
+
value += 1
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func getValue() -> Int {
|
|
45
|
+
return value
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Usage
|
|
50
|
+
let counter = Counter()
|
|
51
|
+
await counter.increment()
|
|
52
|
+
let value = await counter.getValue()
|
|
53
|
+
|
|
54
|
+
// ✅ Actor with async methods
|
|
55
|
+
actor ImageCache {
|
|
56
|
+
private var cache: [String: UIImage] = [:]
|
|
57
|
+
|
|
58
|
+
func image(for key: String) async -> UIImage? {
|
|
59
|
+
if let cached = cache[key] {
|
|
60
|
+
return cached
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let image = await downloadImage(for: key)
|
|
64
|
+
cache[key] = image
|
|
65
|
+
return image
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private func downloadImage(for key: String) async -> UIImage? {
|
|
69
|
+
// Download implementation
|
|
70
|
+
return nil
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ❌ Don't use class with locks when actor is safer
|
|
75
|
+
class UnsafeCache {
|
|
76
|
+
private var cache: [String: UIImage] = [:]
|
|
77
|
+
private let lock = NSLock()
|
|
78
|
+
|
|
79
|
+
func image(for key: String) -> UIImage? {
|
|
80
|
+
lock.lock()
|
|
81
|
+
defer { lock.unlock() }
|
|
82
|
+
return cache[key]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## @MainActor for UI Updates
|
|
88
|
+
|
|
89
|
+
Use @MainActor to ensure code runs on the main thread:
|
|
90
|
+
|
|
91
|
+
```swift
|
|
92
|
+
// ✅ Mark entire class as @MainActor
|
|
93
|
+
@MainActor
|
|
94
|
+
class UserViewModel: ObservableObject {
|
|
95
|
+
@Published var user: User?
|
|
96
|
+
@Published var isLoading = false
|
|
97
|
+
|
|
98
|
+
func loadUser(id: String) async {
|
|
99
|
+
isLoading = true
|
|
100
|
+
defer { isLoading = false }
|
|
101
|
+
|
|
102
|
+
do {
|
|
103
|
+
user = try await fetchUser(id: id)
|
|
104
|
+
} catch {
|
|
105
|
+
print("Error: \(error)")
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ✅ Mark specific methods as @MainActor
|
|
111
|
+
class DataService {
|
|
112
|
+
func fetchData() async -> Data {
|
|
113
|
+
// Background work
|
|
114
|
+
return Data()
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@MainActor
|
|
118
|
+
func updateUI(with data: Data) {
|
|
119
|
+
// UI updates guaranteed on main thread
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ✅ Mark properties as @MainActor
|
|
124
|
+
class ViewModel {
|
|
125
|
+
@MainActor var title: String = ""
|
|
126
|
+
|
|
127
|
+
func loadData() async {
|
|
128
|
+
let data = await fetchData()
|
|
129
|
+
await updateTitle(data.title)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@MainActor
|
|
133
|
+
private func updateTitle(_ newTitle: String) {
|
|
134
|
+
title = newTitle
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ❌ Avoid manual dispatch to main queue
|
|
139
|
+
func badUpdate() async {
|
|
140
|
+
let data = await fetchData()
|
|
141
|
+
DispatchQueue.main.async {
|
|
142
|
+
// Update UI
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Structured Concurrency with Task
|
|
148
|
+
|
|
149
|
+
Use Task for structured concurrent operations:
|
|
150
|
+
|
|
151
|
+
```swift
|
|
152
|
+
// ✅ Simple Task creation
|
|
153
|
+
Task {
|
|
154
|
+
let user = try await fetchUser(id: "123")
|
|
155
|
+
print(user)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ✅ Task with priority
|
|
159
|
+
Task(priority: .high) {
|
|
160
|
+
let urgentData = try await fetchUrgentData()
|
|
161
|
+
process(urgentData)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ✅ Detached task (unstructured)
|
|
165
|
+
Task.detached {
|
|
166
|
+
// Runs independently of current context
|
|
167
|
+
await performBackgroundWork()
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ✅ Task cancellation
|
|
171
|
+
let task = Task {
|
|
172
|
+
try await longRunningOperation()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Later, cancel the task
|
|
176
|
+
task.cancel()
|
|
177
|
+
|
|
178
|
+
// ✅ Check for cancellation
|
|
179
|
+
func processItems(_ items: [Item]) async throws {
|
|
180
|
+
for item in items {
|
|
181
|
+
try Task.checkCancellation() // Throw if cancelled
|
|
182
|
+
await process(item)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ✅ Handle cancellation gracefully
|
|
187
|
+
func fetchWithCancellation() async throws -> Data {
|
|
188
|
+
guard !Task.isCancelled else {
|
|
189
|
+
throw CancellationError()
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return try await URLSession.shared.data(from: url).0
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## TaskGroup for Parallel Operations
|
|
197
|
+
|
|
198
|
+
Use TaskGroup to run multiple async operations in parallel:
|
|
199
|
+
|
|
200
|
+
```swift
|
|
201
|
+
// ✅ Parallel data fetching
|
|
202
|
+
func fetchAllUsers(ids: [String]) async throws -> [User] {
|
|
203
|
+
try await withThrowingTaskGroup(of: User.self) { group in
|
|
204
|
+
for id in ids {
|
|
205
|
+
group.addTask {
|
|
206
|
+
try await fetchUser(id: id)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var users: [User] = []
|
|
211
|
+
for try await user in group {
|
|
212
|
+
users.append(user)
|
|
213
|
+
}
|
|
214
|
+
return users
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ✅ Process results as they complete
|
|
219
|
+
func processImages(_ urls: [URL]) async {
|
|
220
|
+
await withTaskGroup(of: UIImage?.self) { group in
|
|
221
|
+
for url in urls {
|
|
222
|
+
group.addTask {
|
|
223
|
+
await downloadImage(from: url)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
for await image in group {
|
|
228
|
+
if let image = image {
|
|
229
|
+
await display(image)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ✅ Limit concurrency with semaphore pattern
|
|
236
|
+
func processWithLimit(_ items: [Item], limit: Int) async {
|
|
237
|
+
await withTaskGroup(of: Void.self) { group in
|
|
238
|
+
var iterator = items.makeIterator()
|
|
239
|
+
|
|
240
|
+
// Start initial batch
|
|
241
|
+
for _ in 0..<min(limit, items.count) {
|
|
242
|
+
if let item = iterator.next() {
|
|
243
|
+
group.addTask {
|
|
244
|
+
await process(item)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// As tasks complete, start new ones
|
|
250
|
+
for await _ in group {
|
|
251
|
+
if let item = iterator.next() {
|
|
252
|
+
group.addTask {
|
|
253
|
+
await process(item)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ❌ Don't use unstructured concurrency for related tasks
|
|
261
|
+
func badParallelFetch(ids: [String]) async -> [User] {
|
|
262
|
+
var users: [User] = []
|
|
263
|
+
for id in ids {
|
|
264
|
+
Task { // Unstructured - loses parent context
|
|
265
|
+
if let user = try? await fetchUser(id: id) {
|
|
266
|
+
users.append(user) // Data race!
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return users
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## AsyncSequence for Streaming Data
|
|
275
|
+
|
|
276
|
+
Use AsyncSequence for processing streams of data:
|
|
277
|
+
|
|
278
|
+
```swift
|
|
279
|
+
// ✅ Consuming AsyncSequence
|
|
280
|
+
func processNotifications() async {
|
|
281
|
+
for await notification in notificationStream {
|
|
282
|
+
handle(notification)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ✅ Creating custom AsyncSequence
|
|
287
|
+
struct Countdown: AsyncSequence {
|
|
288
|
+
typealias Element = Int
|
|
289
|
+
let start: Int
|
|
290
|
+
|
|
291
|
+
struct AsyncIterator: AsyncIteratorProtocol {
|
|
292
|
+
var current: Int
|
|
293
|
+
|
|
294
|
+
mutating func next() async -> Int? {
|
|
295
|
+
guard current >= 0 else { return nil }
|
|
296
|
+
|
|
297
|
+
let value = current
|
|
298
|
+
current -= 1
|
|
299
|
+
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
300
|
+
return value
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func makeAsyncIterator() -> AsyncIterator {
|
|
305
|
+
AsyncIterator(current: start)
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Usage
|
|
310
|
+
for await number in Countdown(start: 5) {
|
|
311
|
+
print(number)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ✅ Transform AsyncSequence
|
|
315
|
+
func processStream() async {
|
|
316
|
+
let stream = URLSession.shared.bytes(from: url)
|
|
317
|
+
|
|
318
|
+
for try await byte in stream {
|
|
319
|
+
process(byte)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ✅ Async sequence with map/filter
|
|
324
|
+
extension AsyncSequence {
|
|
325
|
+
func mapAsync<T>(_ transform: @escaping (Element) async throws -> T) -> AsyncThrowingStream<T, Error> {
|
|
326
|
+
AsyncThrowingStream { continuation in
|
|
327
|
+
Task {
|
|
328
|
+
do {
|
|
329
|
+
for try await element in self {
|
|
330
|
+
let transformed = try await transform(element)
|
|
331
|
+
continuation.yield(transformed)
|
|
332
|
+
}
|
|
333
|
+
continuation.finish()
|
|
334
|
+
} catch {
|
|
335
|
+
continuation.finish(throwing: error)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Continuations for Bridging Callback APIs
|
|
344
|
+
|
|
345
|
+
Use continuations to wrap callback-based APIs:
|
|
346
|
+
|
|
347
|
+
```swift
|
|
348
|
+
// ✅ Wrap completion handler with continuation
|
|
349
|
+
func fetchData() async throws -> Data {
|
|
350
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
351
|
+
legacyFetchData { result in
|
|
352
|
+
switch result {
|
|
353
|
+
case .success(let data):
|
|
354
|
+
continuation.resume(returning: data)
|
|
355
|
+
case .failure(let error):
|
|
356
|
+
continuation.resume(throwing: error)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ✅ Wrap delegate callback
|
|
363
|
+
func requestPermission() async -> Bool {
|
|
364
|
+
await withCheckedContinuation { continuation in
|
|
365
|
+
permissionManager.request { granted in
|
|
366
|
+
continuation.resume(returning: granted)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ⚠️ Use unsafe continuation only when necessary
|
|
372
|
+
func unsafeExample() async -> String {
|
|
373
|
+
await withUnsafeContinuation { continuation in
|
|
374
|
+
// Must call resume exactly once!
|
|
375
|
+
continuation.resume(returning: "value")
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// ❌ Never resume continuation multiple times
|
|
380
|
+
func badContinuation() async {
|
|
381
|
+
await withCheckedContinuation { continuation in
|
|
382
|
+
continuation.resume(returning: 1)
|
|
383
|
+
continuation.resume(returning: 2) // Runtime error!
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Sendable Protocol
|
|
389
|
+
|
|
390
|
+
Use Sendable to ensure types can be safely passed across concurrency boundaries:
|
|
391
|
+
|
|
392
|
+
```swift
|
|
393
|
+
// ✅ Value types are implicitly Sendable
|
|
394
|
+
struct User: Sendable {
|
|
395
|
+
let id: String
|
|
396
|
+
let name: String
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ✅ Immutable reference types can be Sendable
|
|
400
|
+
final class Configuration: Sendable {
|
|
401
|
+
let apiKey: String
|
|
402
|
+
let baseURL: URL
|
|
403
|
+
|
|
404
|
+
init(apiKey: String, baseURL: URL) {
|
|
405
|
+
self.apiKey = apiKey
|
|
406
|
+
self.baseURL = baseURL
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ✅ Actor types are implicitly Sendable
|
|
411
|
+
actor DatabaseManager: Sendable {
|
|
412
|
+
func save(_ item: Item) async throws {
|
|
413
|
+
// Implementation
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ❌ Mutable classes should not be Sendable
|
|
418
|
+
class MutableState: Sendable { // Warning!
|
|
419
|
+
var count = 0 // Unsafe across concurrency boundaries
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ✅ Use actor instead
|
|
423
|
+
actor SafeState {
|
|
424
|
+
var count = 0
|
|
425
|
+
|
|
426
|
+
func increment() {
|
|
427
|
+
count += 1
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Best Practices
|
|
433
|
+
|
|
434
|
+
### Avoid Data Races
|
|
435
|
+
|
|
436
|
+
```swift
|
|
437
|
+
// ❌ Data race with shared mutable state
|
|
438
|
+
class Counter {
|
|
439
|
+
var value = 0
|
|
440
|
+
|
|
441
|
+
func increment() {
|
|
442
|
+
value += 1 // Unsafe if called from multiple tasks
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ✅ Use actor for shared mutable state
|
|
447
|
+
actor Counter {
|
|
448
|
+
var value = 0
|
|
449
|
+
|
|
450
|
+
func increment() {
|
|
451
|
+
value += 1 // Safe - actor serializes access
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Prefer Structured Concurrency
|
|
457
|
+
|
|
458
|
+
```swift
|
|
459
|
+
// ❌ Unstructured tasks lose parent context
|
|
460
|
+
func badExample() {
|
|
461
|
+
Task {
|
|
462
|
+
await work1()
|
|
463
|
+
}
|
|
464
|
+
Task {
|
|
465
|
+
await work2()
|
|
466
|
+
}
|
|
467
|
+
// Returns immediately, tasks run independently
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ✅ Structured concurrency with async let
|
|
471
|
+
func goodExample() async {
|
|
472
|
+
async let result1 = work1()
|
|
473
|
+
async let result2 = work2()
|
|
474
|
+
|
|
475
|
+
let (r1, r2) = await (result1, result2)
|
|
476
|
+
// Both complete before function returns
|
|
477
|
+
}
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
### Use async let for Independent Operations
|
|
481
|
+
|
|
482
|
+
```swift
|
|
483
|
+
// ✅ Parallel execution with async let
|
|
484
|
+
func loadDashboard() async throws -> Dashboard {
|
|
485
|
+
async let user = fetchUser()
|
|
486
|
+
async let posts = fetchPosts()
|
|
487
|
+
async let stats = fetchStats()
|
|
488
|
+
|
|
489
|
+
return try await Dashboard(
|
|
490
|
+
user: user,
|
|
491
|
+
posts: posts,
|
|
492
|
+
stats: stats
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ❌ Sequential execution (slower)
|
|
497
|
+
func slowDashboard() async throws -> Dashboard {
|
|
498
|
+
let user = try await fetchUser()
|
|
499
|
+
let posts = try await fetchPosts()
|
|
500
|
+
let stats = try await fetchStats()
|
|
501
|
+
|
|
502
|
+
return Dashboard(user: user, posts: posts, stats: stats)
|
|
503
|
+
}
|
|
504
|
+
```
|
|
505
|
+
|
|
506
|
+
### Handle Task Cancellation
|
|
507
|
+
|
|
508
|
+
```swift
|
|
509
|
+
// ✅ Respect cancellation
|
|
510
|
+
func processItems(_ items: [Item]) async throws {
|
|
511
|
+
for item in items {
|
|
512
|
+
try Task.checkCancellation()
|
|
513
|
+
await process(item)
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// ✅ Clean up on cancellation
|
|
518
|
+
func downloadFile() async throws -> URL {
|
|
519
|
+
let task = Task {
|
|
520
|
+
try await actualDownload()
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
defer {
|
|
524
|
+
if Task.isCancelled {
|
|
525
|
+
cleanupPartialDownload()
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return try await task.value
|
|
530
|
+
}
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### Avoid Blocking the Main Thread
|
|
534
|
+
|
|
535
|
+
```swift
|
|
536
|
+
// ❌ Blocking UI thread
|
|
537
|
+
@MainActor
|
|
538
|
+
func loadData() {
|
|
539
|
+
let data = fetchDataSynchronously() // Blocks UI!
|
|
540
|
+
updateUI(data)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ✅ Use async/await
|
|
544
|
+
@MainActor
|
|
545
|
+
func loadData() async {
|
|
546
|
+
let data = await fetchDataAsynchronously()
|
|
547
|
+
updateUI(data) // Already on main actor
|
|
548
|
+
}
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
## Common Patterns
|
|
552
|
+
|
|
553
|
+
### Retry with Backoff
|
|
554
|
+
|
|
555
|
+
```swift
|
|
556
|
+
func fetchWithRetry<T>(
|
|
557
|
+
maxAttempts: Int = 3,
|
|
558
|
+
operation: @escaping () async throws -> T
|
|
559
|
+
) async throws -> T {
|
|
560
|
+
var lastError: Error?
|
|
561
|
+
|
|
562
|
+
for attempt in 0..<maxAttempts {
|
|
563
|
+
do {
|
|
564
|
+
return try await operation()
|
|
565
|
+
} catch {
|
|
566
|
+
lastError = error
|
|
567
|
+
|
|
568
|
+
if attempt < maxAttempts - 1 {
|
|
569
|
+
let delay = UInt64(pow(2.0, Double(attempt)) * 1_000_000_000)
|
|
570
|
+
try await Task.sleep(nanoseconds: delay)
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
throw lastError!
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Usage
|
|
579
|
+
let user = try await fetchWithRetry {
|
|
580
|
+
try await fetchUser(id: "123")
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
### Timeout
|
|
585
|
+
|
|
586
|
+
```swift
|
|
587
|
+
func withTimeout<T>(
|
|
588
|
+
seconds: TimeInterval,
|
|
589
|
+
operation: @escaping () async throws -> T
|
|
590
|
+
) async throws -> T {
|
|
591
|
+
try await withThrowingTaskGroup(of: T.self) { group in
|
|
592
|
+
group.addTask {
|
|
593
|
+
try await operation()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
group.addTask {
|
|
597
|
+
try await Task.sleep(nanoseconds: UInt64(seconds * 1_000_000_000))
|
|
598
|
+
throw TimeoutError()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let result = try await group.next()!
|
|
602
|
+
group.cancelAll()
|
|
603
|
+
return result
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Usage
|
|
608
|
+
let user = try await withTimeout(seconds: 5) {
|
|
609
|
+
try await fetchUser(id: "123")
|
|
610
|
+
}
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
### Debounce
|
|
614
|
+
|
|
615
|
+
```swift
|
|
616
|
+
actor Debouncer<T: Sendable> {
|
|
617
|
+
private var task: Task<T, Error>?
|
|
618
|
+
private let delay: Duration
|
|
619
|
+
private let operation: @Sendable () async throws -> T
|
|
620
|
+
|
|
621
|
+
init(delay: Duration, operation: @escaping @Sendable () async throws -> T) {
|
|
622
|
+
self.delay = delay
|
|
623
|
+
self.operation = operation
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
func call() async throws -> T {
|
|
627
|
+
task?.cancel()
|
|
628
|
+
|
|
629
|
+
let newTask = Task {
|
|
630
|
+
try await Task.sleep(for: delay)
|
|
631
|
+
return try await operation()
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
task = newTask
|
|
635
|
+
return try await newTask.value
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Usage
|
|
640
|
+
let searchDebouncer = Debouncer(delay: .milliseconds(300)) {
|
|
641
|
+
await searchAPI(query: currentQuery)
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Only last call within 300ms executes
|
|
645
|
+
try await searchDebouncer.call()
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
---
|
|
649
|
+
|
|
650
|
+
**Sources:**
|
|
651
|
+
- [Swift Evolution: Concurrency](https://github.com/apple/swift-evolution/blob/main/proposals/0296-async-await.md)
|
|
652
|
+
- [Swift.org: Concurrency](https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html)
|
|
653
|
+
- [WWDC21: Meet async/await in Swift](https://developer.apple.com/videos/play/wwdc2021/10132/)
|
|
654
|
+
- [WWDC21: Protect mutable state with Swift actors](https://developer.apple.com/videos/play/wwdc2021/10133/)
|