|
| 1 | +--- |
| 2 | +name: swift-concurrency |
| 3 | +description: Expert guidance on Swift Concurrency concepts. Use when working with async/await, Tasks, actors, MainActor, Sendable, isolation domains, or debugging concurrency compiler errors. Helps write safe concurrent Swift code. |
| 4 | +--- |
| 5 | + |
| 6 | +# Swift Concurrency Skill |
| 7 | + |
| 8 | +This skill provides expert guidance on Swift's concurrency system based on the mental models from [Fucking Approachable Swift Concurrency](https://fuckingapproachableswiftconcurrency.com). |
| 9 | + |
| 10 | +## Core Mental Model: The Office Building |
| 11 | + |
| 12 | +Think of your app as an office building where **isolation domains** are private offices with locks: |
| 13 | + |
| 14 | +- **MainActor** = Front desk (handles all UI interactions, only one exists) |
| 15 | +- **actor** types = Department offices (Accounting, Legal, HR - each protects its own data) |
| 16 | +- **nonisolated** code = Hallways (shared space, no private documents) |
| 17 | +- **Sendable** types = Photocopies (safe to share between offices) |
| 18 | +- **Non-Sendable** types = Original documents (must stay in one office) |
| 19 | + |
| 20 | +You can't barge into someone's office. You knock (`await`) and wait. |
| 21 | + |
| 22 | +## Async/Await |
| 23 | + |
| 24 | +An `async` function can pause. Use `await` to suspend until work finishes: |
| 25 | + |
| 26 | +```swift |
| 27 | +func fetchUser(id: Int) async throws -> User { |
| 28 | + let (data, _) = try await URLSession.shared.data(from: url) |
| 29 | + return try JSONDecoder().decode(User.self, from: data) |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +For parallel work, use `async let`: |
| 34 | + |
| 35 | +```swift |
| 36 | +async let avatar = fetchImage("avatar.jpg") |
| 37 | +async let banner = fetchImage("banner.jpg") |
| 38 | +return Profile(avatar: try await avatar, banner: try await banner) |
| 39 | +``` |
| 40 | + |
| 41 | +## Tasks |
| 42 | + |
| 43 | +A `Task` is a unit of async work you can manage: |
| 44 | + |
| 45 | +```swift |
| 46 | +// SwiftUI - cancels when view disappears |
| 47 | +.task { avatar = await downloadAvatar() } |
| 48 | + |
| 49 | +// Manual task creation |
| 50 | +Task { await saveProfile() } |
| 51 | + |
| 52 | +// Parallel work with TaskGroup |
| 53 | +try await withThrowingTaskGroup(of: Void.self) { group in |
| 54 | + group.addTask { avatar = try await downloadAvatar() } |
| 55 | + group.addTask { bio = try await fetchBio() } |
| 56 | + try await group.waitForAll() |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +Child tasks in a group: cancellation propagates, errors cancel siblings, waits for all to complete. |
| 61 | + |
| 62 | +## Isolation Domains |
| 63 | + |
| 64 | +Swift asks "who can access this data?" not "which thread?". Three isolation domains: |
| 65 | + |
| 66 | +### 1. MainActor |
| 67 | + |
| 68 | +For UI. Everything UI-related should be here: |
| 69 | + |
| 70 | +```swift |
| 71 | +@MainActor |
| 72 | +class ViewModel { |
| 73 | + var items: [Item] = [] // Protected by MainActor |
| 74 | +} |
| 75 | +``` |
| 76 | + |
| 77 | +### 2. Actors |
| 78 | + |
| 79 | +Protect their own mutable state with exclusive access: |
| 80 | + |
| 81 | +```swift |
| 82 | +actor BankAccount { |
| 83 | + var balance: Double = 0 |
| 84 | + func deposit(_ amount: Double) { balance += amount } |
| 85 | +} |
| 86 | + |
| 87 | +await account.deposit(100) // Must await from outside |
| 88 | +``` |
| 89 | + |
| 90 | +### 3. Nonisolated |
| 91 | + |
| 92 | +Opts out of actor isolation. Cannot access actor's protected state: |
| 93 | + |
| 94 | +```swift |
| 95 | +actor BankAccount { |
| 96 | + nonisolated func bankName() -> String { "Acme Bank" } |
| 97 | +} |
| 98 | +let name = account.bankName() // No await needed |
| 99 | +``` |
| 100 | + |
| 101 | +## Approachable Concurrency (Swift 6.2+) |
| 102 | + |
| 103 | +Two build settings that simplify the mental model: |
| 104 | + |
| 105 | +- **SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor**: Everything runs on MainActor unless you say otherwise |
| 106 | +- **SWIFT_APPROACHABLE_CONCURRENCY = YES**: nonisolated async functions stay on caller's actor |
| 107 | + |
| 108 | +```swift |
| 109 | +// Runs on MainActor (default) |
| 110 | +func updateUI() async { } |
| 111 | + |
| 112 | +// Runs on background (opt-in) |
| 113 | +@concurrent func processLargeFile() async { } |
| 114 | +``` |
| 115 | + |
| 116 | +## Sendable |
| 117 | + |
| 118 | +Marks types safe to pass across isolation boundaries: |
| 119 | + |
| 120 | +```swift |
| 121 | +// Sendable - value type, each gets a copy |
| 122 | +struct User: Sendable { |
| 123 | + let id: Int |
| 124 | + let name: String |
| 125 | +} |
| 126 | + |
| 127 | +// Non-Sendable - mutable class state |
| 128 | +class Counter { |
| 129 | + var count = 0 |
| 130 | +} |
| 131 | +``` |
| 132 | + |
| 133 | +Automatically Sendable: |
| 134 | +- Structs/enums with only Sendable properties |
| 135 | +- Actors (protect their own state) |
| 136 | +- @MainActor types (MainActor serializes access) |
| 137 | + |
| 138 | +For thread-safe classes with internal synchronization: |
| 139 | + |
| 140 | +```swift |
| 141 | +final class ThreadSafeCache: @unchecked Sendable { |
| 142 | + private let lock = NSLock() |
| 143 | + private var storage: [String: Data] = [:] |
| 144 | +} |
| 145 | +``` |
| 146 | + |
| 147 | +## Isolation Inheritance |
| 148 | + |
| 149 | +With Approachable Concurrency, isolation flows from MainActor through your code: |
| 150 | + |
| 151 | +- **Functions**: Inherit caller's isolation unless explicitly marked |
| 152 | +- **Closures**: Inherit from context where defined |
| 153 | +- **Task { }**: Inherits actor isolation from creation site |
| 154 | +- **Task.detached { }**: No inheritance (rarely needed) |
| 155 | + |
| 156 | +## Common Mistakes to Avoid |
| 157 | + |
| 158 | +### 1. Thinking async = background |
| 159 | + |
| 160 | +```swift |
| 161 | +// Still blocks main thread! |
| 162 | +@MainActor func slowFunction() async { |
| 163 | + let result = expensiveCalculation() // Synchronous = blocking |
| 164 | +} |
| 165 | +// Fix: Use @concurrent for CPU-heavy work |
| 166 | +``` |
| 167 | + |
| 168 | +### 2. Creating too many actors |
| 169 | + |
| 170 | +Most things can live on MainActor. Only create actors when you have shared mutable state that can't be on MainActor. |
| 171 | + |
| 172 | +### 3. Making everything Sendable |
| 173 | + |
| 174 | +Not everything needs to cross boundaries. Step back and ask if data actually moves between isolation domains. |
| 175 | + |
| 176 | +### 4. Using MainActor.run unnecessarily |
| 177 | + |
| 178 | +```swift |
| 179 | +// Unnecessary |
| 180 | +await MainActor.run { self.data = data } |
| 181 | + |
| 182 | +// Better - annotate the function |
| 183 | +@MainActor func loadData() async { self.data = await fetchData() } |
| 184 | +``` |
| 185 | + |
| 186 | +### 5. Blocking the cooperative thread pool |
| 187 | + |
| 188 | +Never use DispatchSemaphore, DispatchGroup.wait() in async code. Risks deadlock. |
| 189 | + |
| 190 | +### 6. Creating unnecessary Tasks |
| 191 | + |
| 192 | +```swift |
| 193 | +// Bad - unstructured |
| 194 | +Task { await fetchUsers() } |
| 195 | +Task { await fetchPosts() } |
| 196 | + |
| 197 | +// Good - structured concurrency |
| 198 | +async let users = fetchUsers() |
| 199 | +async let posts = fetchPosts() |
| 200 | +await (users, posts) |
| 201 | +``` |
| 202 | + |
| 203 | +## Quick Reference |
| 204 | + |
| 205 | +| Keyword | Purpose | |
| 206 | +|---------|---------| |
| 207 | +| `async` | Function can pause | |
| 208 | +| `await` | Pause here until done | |
| 209 | +| `Task { }` | Start async work, inherits context | |
| 210 | +| `Task.detached { }` | Start async work, no context | |
| 211 | +| `@MainActor` | Runs on main thread | |
| 212 | +| `actor` | Type with isolated mutable state | |
| 213 | +| `nonisolated` | Opts out of actor isolation | |
| 214 | +| `Sendable` | Safe to pass between isolation domains | |
| 215 | +| `@concurrent` | Always run on background (Swift 6.2+) | |
| 216 | +| `async let` | Start parallel work | |
| 217 | +| `TaskGroup` | Dynamic parallel work | |
| 218 | + |
| 219 | +## When the Compiler Complains |
| 220 | + |
| 221 | +Trace the isolation: Where did it come from? Where is code trying to run? What data crosses a boundary? |
| 222 | + |
| 223 | +The answer is usually obvious once you ask the right question. |
| 224 | + |
| 225 | +## Further Reading |
| 226 | + |
| 227 | +- [Matt Massicotte's Blog](https://www.massicotte.org/) - The source of these mental models |
| 228 | +- [Swift Concurrency Documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) |
| 229 | +- [WWDC21: Meet async/await](https://developer.apple.com/videos/play/wwdc2021/10132/) |
| 230 | +- [WWDC21: Protect mutable state with actors](https://developer.apple.com/videos/play/wwdc2021/10133/) |
0 commit comments