--- name: axiom-swift-concurrency-ref description: Swift concurrency API reference — actors, Sendable, Task/TaskGroup, AsyncStream, continuations, isolation patterns, DispatchQueue-to-actor migration with gotcha tables license: MIT metadata: version: "1.0.0" last-updated: "2026-02-26" --- # Swift Concurrency API Reference Complete Swift concurrency API reference for copy-paste patterns and syntax lookup. Complements `axiom-swift-concurrency` (which covers *when* and *why* to use concurrency — progressive journey, decision trees, @concurrent, isolated conformances). **Related skills**: `axiom-swift-concurrency` (progressive journey, decision trees), `axiom-synchronization` (Mutex, locks), `axiom-assume-isolated` (assumeIsolated patterns) ## Part 1: Actor Patterns ### Actor Definition ```swift actor ImageCache { private var cache: [URL: UIImage] = [:] func image(for url: URL) -> UIImage? { cache[url] } func store(_ image: UIImage, for url: URL) { cache[url] = image } } // Usage — must await across isolation boundary let cache = ImageCache() let image = await cache.image(for: url) ``` All properties and methods on an actor are isolated by default. Callers outside the actor's isolation domain must use `await` to access them. ### Actor Isolation Rules Every actor's stored properties and methods are isolated to that actor. Access from outside the isolation boundary requires `await`, which suspends the caller until the actor can process the request. ```swift actor Counter { var count = 0 // Isolated — external access requires await let name: String // let constants are implicitly nonisolated func increment() { // Isolated — await required from outside count += 1 } nonisolated func identity() -> String { name // OK: accessing nonisolated let } } let counter = Counter(name: "main") await counter.increment() // Must await across isolation boundary let id = counter.identity() // No await needed — nonisolated ``` ### nonisolated Keyword Opt out of isolation for synchronous access to non-mutable state. ```swift actor MyActor { let id: UUID // let constants are implicitly nonisolated nonisolated var description: String { "Actor \(id)" // Can only access nonisolated state } nonisolated func hash(into hasher: inout Hasher) { hasher.combine(id) // Only nonisolated properties } } ``` `nonisolated` methods cannot access any isolated stored properties. Use this for protocol conformances (like `Hashable`, `CustomStringConvertible`) that require synchronous access. ### Actor Reentrancy Suspension points (`await`) inside an actor allow other callers to interleave. State may change between any two `await` expressions. ```swift actor BankAccount { var balance: Double = 0 func transfer(amount: Double, to other: BankAccount) async { guard balance >= amount else { return } balance -= amount // REENTRANCY HAZARD: another caller could modify balance here // while we await the deposit on the other actor await other.deposit(amount) } func deposit(_ amount: Double) { balance += amount } } ``` **Pattern**: Re-check state after every `await` inside an actor: ```swift actor BankAccount { var balance: Double = 0 func transfer(amount: Double, to other: BankAccount) async -> Bool { guard balance >= amount else { return false } balance -= amount await other.deposit(amount) // Re-check invariants after await if needed return true } } ``` ### Global Actors A global actor provides a single shared isolation domain accessible from anywhere. ```swift @globalActor actor MyGlobalActor { static let shared = MyGlobalActor() } @MyGlobalActor func doWork() { /* isolated to MyGlobalActor */ } @MyGlobalActor class MyService { var state: Int = 0 // Isolated to MyGlobalActor } ``` ### @MainActor The built-in global actor for UI work. All UI updates must happen on `@MainActor`. ```swift @MainActor class ViewModel: ObservableObject { @Published var items: [Item] = [] func loadItems() async { let data = await fetchFromNetwork() items = data // Safe: already on MainActor } } // Annotate individual members class MixedService { @MainActor var uiState: String = "" @MainActor func updateUI() { uiState = "Done" } func backgroundWork() async -> String { await heavyComputation() } } ``` **Subclass inheritance**: If a class is `@MainActor`, all subclasses inherit that isolation. ### Actor Init Actor initializers are NOT isolated to the actor. You cannot call isolated methods from init. ```swift actor DataManager { var data: [String] = [] init() { // Cannot call isolated methods here // self.loadDefaults() // ERROR: actor-isolated method in non-isolated init } // Use a factory method instead static func create() async -> DataManager { let manager = DataManager() await manager.loadDefaults() return manager } func loadDefaults() { data = ["default"] } } ``` ### Actor Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | Actor reentrancy | State changes between awaits | Re-check state after each await | | nonisolated accessing isolated state | Compiler error | Remove nonisolated or make property nonisolated | | Calling actor method from sync context | "Expression is 'async'" | Wrap in Task {} or make caller async | | Global actor inheritance | Subclass inherits @MainActor | Be intentional about which methods need isolation | | Actor init not isolated | Can't call isolated methods in init | Use factory method or populate after init | | Actor protocol conformance | "Non-isolated" conformance error | Use nonisolated for protocol methods, or isolated conformance (Swift 6.2+) | | Using actor for ViewModel | @Published won't work, UI updates require await | Use @MainActor class for UI-facing code, actor only for non-UI shared state | | GCD queue-hopping inside actor | Breaks isolation guarantees, risks thread explosion | Remove GCD — actor isolation already serializes access | --- ## Part 2: Sendable Patterns ### Automatic Sendable Conformance Value types are Sendable when all stored properties are Sendable. ```swift // Structs: Sendable when all stored properties are Sendable struct UserProfile: Sendable { let name: String let age: Int } // Enums: Sendable when all associated values are Sendable enum LoadState: Sendable { case idle case loading case loaded(String) // String is Sendable case failed(Error) // ERROR: Error is not Sendable } // Fix: use a Sendable error type enum LoadState: Sendable { case idle case loading case loaded(String) case failed(any Error & Sendable) } ``` ### @Sendable Closures Closures passed across isolation boundaries must be `@Sendable`. A `@Sendable` closure cannot capture mutable local state. ```swift func runInBackground(_ work: @Sendable () -> Void) { Task.detached { work() } } // All captured values must be Sendable var count = 0 runInBackground { // ERROR: capture of mutable local variable // count += 1 } let snapshot = count runInBackground { print(snapshot) // OK: let binding of Sendable type } ``` ### @unchecked Sendable Manual guarantee of thread safety. Use only when you provide synchronization yourself. ```swift final class ThreadSafeCache: @unchecked Sendable { private let lock = NSLock() private var storage: [String: Any] = [:] func get(_ key: String) -> Any? { lock.lock() defer { lock.unlock() } return storage[key] } func set(_ key: String, value: Any) { lock.lock() defer { lock.unlock() } storage[key] = value } } ``` #### Requirements for @unchecked Sendable - Class must be `final` - All mutable state must be protected by a synchronization primitive (lock, queue, Mutex) - You are responsible for correctness — the compiler will not check ### Conditional Conformance ```swift struct Box { let value: T } // Box is Sendable only when T is Sendable extension Box: Sendable where T: Sendable {} // Standard library uses this extensively: // Array: Sendable where Element: Sendable // Dictionary: Sendable where Key: Sendable, Value: Sendable // Optional: Sendable where Wrapped: Sendable ``` ### sending Parameter Modifier (SE-0430) Transfer ownership of a value across isolation boundaries. The caller gives up access. ```swift func process(_ value: sending String) async { // Caller can no longer access value after this call await store(value) } // Useful for transferring non-Sendable types when caller won't use them again func handOff(_ connection: sending NetworkConnection) async { await manager.accept(connection) } ``` ### Build Settings Control the strictness of Sendable checking in Xcode: | Setting | Value | Behavior | |---|---|---| | `SWIFT_STRICT_CONCURRENCY` | `minimal` | Only explicit Sendable annotations checked | | `SWIFT_STRICT_CONCURRENCY` | `targeted` | Inferred Sendable + closure checking | | `SWIFT_STRICT_CONCURRENCY` | `complete` | Full strict concurrency (Swift 6 default) | ### Sendable Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | Class can't be Sendable | "Class cannot conform to Sendable" | Make final + immutable, or @unchecked Sendable with locks | | Closure captures non-Sendable | "Capture of non-Sendable type" | Copy value before capture, or make type Sendable | | Protocol can't require Sendable | Generic constraints complex | Use `where T: Sendable` | | @unchecked Sendable hides bugs | Data races at runtime | Only use when lock/queue guarantees safety | | Array/Dictionary conditional | Collection is Sendable only if Element is | Ensure element types are Sendable | | Error not Sendable | "Type does not conform to Sendable" | Use `any Error & Sendable` or typed errors | --- ## Part 3: Task Management ### Task { } Creates an unstructured task that inherits the current actor context and priority. ```swift // Inherits actor context — if called from @MainActor, runs on MainActor let task = Task { try await fetchData() } // Get the result let result = try await task.value // Get Result let outcome = await task.result ``` ### Task.detached { } Creates a task with no inherited context. Does not inherit the actor or priority. ```swift Task.detached(priority: .background) { // NOT on MainActor even if created from MainActor await processLargeFile() } ``` **When to use**: Background work that must NOT run on the calling actor. Prefer `Task {}` in most cases — `Task.detached` is rarely needed. ### Task Cancellation Cancellation is cooperative. Setting cancellation is a request; the task must check and respond. ```swift let task = Task { for item in largeCollection { // Option 1: Check boolean if Task.isCancelled { break } // Option 2: Throw CancellationError try Task.checkCancellation() await process(item) } } // Request cancellation task.cancel() ``` ### Task.sleep Suspends the current task for a duration. Supports cancellation — throws `CancellationError` if cancelled during sleep. ```swift // Duration-based (preferred) try await Task.sleep(for: .seconds(2)) try await Task.sleep(for: .milliseconds(500)) // Nanoseconds (older API) try await Task.sleep(nanoseconds: 2_000_000_000) ``` ### Task.yield Voluntarily yields execution to allow other tasks to run. Use in long-running synchronous loops. ```swift for i in 0..<1_000_000 { if i.isMultiple(of: 1000) { await Task.yield() } process(i) } ``` ### Task Priority | Priority | Use Case | |---|---| | `.userInitiated` | Direct user action, visible result | | `.high` | Same as .userInitiated | | `.medium` | Default when not specified | | `.low` | Prefetching, non-urgent work | | `.utility` | Long computation, progress shown | | `.background` | Maintenance, cleanup, not time-sensitive | ```swift Task(priority: .userInitiated) { await loadVisibleContent() } Task(priority: .background) { await cleanupTempFiles() } ``` ### @TaskLocal Task-scoped values that propagate to child tasks automatically. ```swift enum RequestContext { @TaskLocal static var requestID: String? @TaskLocal static var userID: String? } // Set values for a scope RequestContext.$requestID.withValue("req-123") { RequestContext.$userID.withValue("user-456") { // Both values available here and in child tasks Task { print(RequestContext.requestID) // "req-123" print(RequestContext.userID) // "user-456" } } } // Outside scope — values are nil print(RequestContext.requestID) // nil ``` **Propagation rules**: `@TaskLocal` values propagate to child tasks created with `Task {}`. They do NOT propagate to `Task.detached {}`. ### Task Timeout Pattern Enforce a deadline on any async operation using a task group race: ```swift func withTimeout( _ duration: Duration, operation: @Sendable @escaping () async throws -> T ) async throws -> T { try await withThrowingTaskGroup(of: T.self) { group in group.addTask { try await operation() } group.addTask { try await Task.sleep(for: duration) throw TimeoutError() } guard let result = try await group.next() else { throw TimeoutError() } group.cancelAll() // Cancel the loser — without this it keeps running return result } } ``` `group.cancelAll()` is critical. Without it, the losing task (either the timeout or the operation) continues running until the group scope exits. ### Task Retain Cycles Tasks capture variables like closures. Stored tasks that reference `self` create retain cycles. ```swift // ❌ Retain cycle: self → task → self task = Task { while true { await self.poll() } } // ✅ Weak capture breaks the cycle task = Task { [weak self] in while let self, !Task.isCancelled { await self.poll() } } ``` **Rule**: Use `[weak self]` when the Task is stored as a property or iterates an infinite async sequence. Short-lived Tasks that complete quickly can use strong captures. ### Thread.current in Swift 6 `Thread.current` is unavailable from async contexts in Swift 6 language mode: ```swift // ❌ Compiler error in Swift 6 mode func check() async { print(Thread.current) } // ✅ Workaround for debugging only extension Thread { static var currentThread: Thread { Thread.current } } ``` Don't rely on thread identity for correctness — tasks move between threads at suspension points. Reason about isolation domains instead. ### Task Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | Task never cancelled | Resource leak, work continues after view disappears | Store task, cancel in deinit/onDisappear | | Ignoring cancellation | Task runs to completion even when cancelled | Check Task.isCancelled in loops, use checkCancellation() | | Task.detached loses actor context | "Not isolated to MainActor" | Use Task {} when you need actor isolation | | Capturing self in stored Task | Retain cycle, deinit never called | Use [weak self] for long-lived or stored tasks | | Assuming async = background | Code stays on calling actor | Use @concurrent to force background execution | | TaskLocal not propagated | Value is nil in detached task | TaskLocal only propagates to child tasks, not detached | | Task priority inversion | Low-priority task blocks high-priority | System handles most cases; avoid awaiting low-priority from high | | Thread.current in async context | Compiler error in Swift 6 mode | Don't rely on thread identity — use isolation domains | --- ## Part 4: Structured Concurrency ### async let Run a fixed number of operations in parallel. All `async let` bindings are implicitly awaited when the scope exits. ```swift async let images = fetchImages() async let metadata = fetchMetadata() async let config = loadConfig() // All three run concurrently, await together let (imgs, meta, cfg) = try await (images, metadata, config) ``` **Semantics**: If one `async let` throws, the others are cancelled. All must complete (or be cancelled) before the enclosing scope exits. ### TaskGroup — Non-Throwing Dynamic number of parallel tasks where none throw. ```swift let results = await withTaskGroup(of: String.self) { group in for name in names { group.addTask { await fetchGreeting(for: name) } } var greetings: [String] = [] for await greeting in group { greetings.append(greeting) } return greetings } ``` ### TaskGroup — Throwing Dynamic number of parallel tasks that can throw. ```swift let images = try await withThrowingTaskGroup(of: (URL, UIImage).self) { group in for url in urls { group.addTask { let image = try await downloadImage(url) return (url, image) } } var results: [URL: UIImage] = [:] for try await (url, image) in group { results[url] = image } return results } ``` ### withDiscardingTaskGroup (iOS 17+) For when you need concurrency but don't need to collect results. More memory-efficient than regular TaskGroup — no result storage. ```swift try await withThrowingDiscardingTaskGroup { group in for connection in connections { group.addTask { try await connection.monitor() // Results are discarded — useful for long-running services } } // Group stays alive until all tasks complete or one throws } ``` #### Real-world pattern — merge multiple notification streams ```swift extension NotificationCenter { func notifications(named names: [Notification.Name]) -> AsyncStream { AsyncStream { continuation in let task = Task { await withDiscardingTaskGroup { group in for name in names { group.addTask { for await _ in self.notifications(named: name) { continuation.yield() } } } } continuation.finish() } continuation.onTermination = { _ in task.cancel() } } } } ``` ### TaskGroup Control ```swift await withTaskGroup(of: Data.self) { group in // Add tasks conditionally group.addTaskUnlessCancelled { await fetchData() } // Cancel remaining tasks group.cancelAll() // Wait without collecting await group.waitForAll() // Iterate one at a time while let result = await group.next() { process(result) } } ``` ### Task Tree Semantics Structured concurrency forms a tree: - **Parent cancellation cancels all children** — cancelling a task cancels all `async let` and TaskGroup children - **Child error propagates to parent** — in throwing groups, a child error cancels siblings and propagates up - **All children must complete before parent returns** — the scope awaits all children, even cancelled ones ```swift // If fetchImages() throws, fetchMetadata() is automatically cancelled async let images = fetchImages() async let metadata = fetchMetadata() let result = try await (images, metadata) ``` ### Structured Concurrency Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | async let unused | Work still executes but result is discarded silently | Assign all async let results or use withDiscardingTaskGroup | | TaskGroup accumulating memory | Memory grows with 10K+ tasks | Process results as they arrive, don't collect all | | Capturing mutable state in addTask | "Mutation of captured var" | Use let binding or actor | | Not handling partial failure | Some tasks succeed, some fail | Use group.next() and handle errors individually | | async let in loop | Compiler error — async let must be in fixed positions | Use TaskGroup instead | | Returning from group early | Remaining tasks still run | Call group.cancelAll() before returning | --- ## Part 5: Async Sequences ### AsyncStream Non-throwing stream for producing values over time. ```swift let stream = AsyncStream { continuation in for i in 0..<10 { continuation.yield(i) } continuation.finish() } for await value in stream { print(value) } ``` ### AsyncThrowingStream Stream that can fail with an error. ```swift let stream = AsyncThrowingStream { continuation in let monitor = NetworkMonitor() monitor.onData = { data in continuation.yield(data) } monitor.onError = { error in continuation.finish(throwing: error) } monitor.onComplete = { continuation.finish() } continuation.onTermination = { @Sendable _ in monitor.stop() } monitor.start() } do { for try await data in stream { process(data) } } catch { handleStreamError(error) } ``` ### Continuation API ```swift let stream = AsyncStream { continuation in // Emit a value continuation.yield(value) // End the stream normally continuation.finish() // Cleanup when consumer cancels or stream ends continuation.onTermination = { @Sendable termination in switch termination { case .cancelled: cleanup() case .finished: finalCleanup() @unknown default: break } } } // For throwing streams let stream = AsyncThrowingStream { continuation in continuation.yield(value) continuation.finish() // Normal end continuation.finish(throwing: error) // End with error } ``` ### Buffering Policies Control what happens when values are produced faster than consumed. ```swift // Keep all values (default) — memory can grow unbounded let stream = AsyncStream(bufferingPolicy: .unbounded) { continuation in // ... } // Keep oldest N values, drop new ones when buffer is full let stream = AsyncStream(bufferingPolicy: .bufferingOldest(100)) { continuation in // ... } // Keep newest N values, drop old ones when buffer is full let stream = AsyncStream(bufferingPolicy: .bufferingNewest(100)) { continuation in // ... } ``` | Policy | Behavior | Use When | |---|---|---| | `.unbounded` | Keeps all values | Consumer keeps up, or bounded producer | | `.bufferingOldest(N)` | Drops new values when full | Order matters, older values have priority | | `.bufferingNewest(N)` | Drops old values when full | Latest state matters (UI updates, sensor data) | ### Custom AsyncSequence ```swift struct Counter: AsyncSequence { typealias Element = Int let limit: Int struct AsyncIterator: AsyncIteratorProtocol { var current = 0 let limit: Int mutating func next() async -> Int? { guard current < limit else { return nil } defer { current += 1 } return current } } func makeAsyncIterator() -> AsyncIterator { AsyncIterator(limit: limit) } } // Usage for await number in Counter(limit: 5) { print(number) // 0, 1, 2, 3, 4 } ``` ### AsyncSequence Operators Standard operators work on any `AsyncSequence`: ```swift // Map for await name in users.map(\.name) { } // Filter for await adult in users.filter({ $0.age >= 18 }) { } // CompactMap for await image in urls.compactMap({ await tryLoadImage($0) }) { } // Prefix for await first5 in stream.prefix(5) { } // first(where:) let match = await stream.first(where: { $0 > threshold }) // Contains let hasMatch = await stream.contains(where: { $0 > threshold }) // Reduce let sum = await numbers.reduce(0, +) ``` ### Built-in Async Sequences ```swift // NotificationCenter for await notification in NotificationCenter.default.notifications(named: .didUpdate) { handleUpdate(notification) } // URLSession bytes let (bytes, response) = try await URLSession.shared.bytes(from: url) for try await byte in bytes { process(byte) } // FileHandle bytes for try await line in FileHandle.standardInput.bytes.lines { process(line) } ``` ### Async Sequence Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | Continuation yielded after finish | Runtime warning, value lost | Track finished state, guard before yield | | Stream never finishing | for-await loop hangs forever | Always call continuation.finish() in all code paths | | No onTermination handler | Resource leak when consumer cancels | Set continuation.onTermination for cleanup | | Unbounded buffer | Memory growth under load | Use .bufferingNewest(N) or .bufferingOldest(N) | | Multiple consumers | Only first consumer gets values | AsyncStream is single-consumer; create separate streams per consumer | | for-await on MainActor | UI freezes waiting for values | Use Task {} to consume off the main path | --- ## Part 6: Isolation Patterns ### @MainActor on Functions ```swift @MainActor func updateUI() { label.text = "Done" } // Call from async context func doWork() async { let result = await computeResult() await updateUI() // Hops to MainActor } ``` ### MainActor.run Explicitly execute a closure on the main actor from any context. ```swift func processData() async { let result = await heavyComputation() await MainActor.run { self.label.text = result self.progressView.isHidden = true } } ``` ### MainActor.assumeIsolated (iOS 17+) Assert that code is already running on the main actor. Crashes at runtime if the assertion is false. ```swift func legacyCallback() { // We KNOW this is called on main thread (UIKit guarantee) MainActor.assumeIsolated { self.viewModel.update() // Access @MainActor state } } ``` See `axiom-assume-isolated` for comprehensive patterns. ### nonisolated Opt out of the enclosing actor's isolation. ```swift @MainActor class ViewModel { let id: UUID // Implicitly nonisolated (let) nonisolated var analyticsID: String { // Explicitly nonisolated id.uuidString } var items: [Item] = [] // Isolated to MainActor } ``` ### nonisolated(unsafe) Compiler escape hatch. Tells the compiler to treat a property as if it's not isolated, without any safety guarantees. ```swift // Use only when you have external guarantees of thread safety nonisolated(unsafe) var legacyState: Int = 0 // Common for global constants that the compiler can't verify nonisolated(unsafe) let formatter: DateFormatter = { let f = DateFormatter() f.dateStyle = .medium return f }() ``` **Warning**: `nonisolated(unsafe)` provides zero runtime protection. Data races will not be caught. Use only as a last resort for bridging legacy code. ### @preconcurrency Suppress concurrency warnings for pre-concurrency APIs during migration. ```swift // Suppress warnings for entire module @preconcurrency import MyLegacyFramework // Suppress for specific protocol conformance class MyDelegate: @preconcurrency SomeLegacyDelegate { func delegateCallback() { // No Sendable warnings for this conformance } } ``` ### #isolation (Swift 6.0+) Capture the caller's isolation context so a function runs on whatever actor the caller is on. ```swift func doWork(isolation: isolated (any Actor)? = #isolation) async { // Runs on caller's actor — no hop if caller is already isolated performWork() } // Called from @MainActor — runs on MainActor @MainActor func setup() async { await doWork() // doWork runs on MainActor } // Called from custom actor — runs on that actor actor MyActor { func run() async { await doWork() // doWork runs on MyActor } } ``` ### #isolation Capture in Task Closures (SE-0420) When spawning `Task` closures that need to work with non-Sendable types, capture the isolation parameter to inherit the caller's context. ```swift func process( delegate: NonSendableDelegate, isolation: isolated (any Actor)? = #isolation ) { Task { _ = isolation // Forces capture — Task inherits caller's isolation delegate.doWork() // ✅ Safe: running on caller's actor } } ``` **Why `_ = isolation` is required**: Per SE-0420, `Task` closures only inherit isolation when a non-optional binding of an isolated parameter is captured by the closure. The `_ = isolation` statement forces this capture. Without it, the Task runs on the default executor and the non-Sendable capture is a compiler error. **When to use**: Spawning Tasks that work with non-Sendable delegate objects, fire-and-forget async work that needs access to caller's state, or bridging callback-based APIs while keeping delegates alive. ### Isolation Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | MainActor.run from MainActor | Unnecessary hop, potential deadlock risk | Check context or use assumeIsolated | | nonisolated(unsafe) data race | Crash at runtime, corrupted state | Use proper isolation or Mutex | | @preconcurrency hiding real issues | Runtime crashes in production | Migrate to proper concurrency before shipping | | #isolation not available pre-5.9 | Compiler error | Use traditional @MainActor annotation | | #isolation not captured in Task | Non-Sendable capture error | Add `_ = isolation` inside Task closure (SE-0420) | | nonisolated on actor method | Can't access any isolated state | Only use for computed properties from non-isolated state | | Thread.current in async context | Compiler error in Swift 6 mode | Don't rely on thread identity — reason about isolation domains | --- ## Part 7: Continuations Bridge callback-based APIs to async/await. ### withCheckedContinuation Non-throwing bridge. ```swift func currentLocation() async -> CLLocation { await withCheckedContinuation { continuation in locationManager.requestLocation { location in continuation.resume(returning: location) } } } ``` ### withCheckedThrowingContinuation Throwing bridge. ```swift func fetchUser(id: String) async throws -> User { try await withCheckedThrowingContinuation { continuation in api.fetchUser(id: id) { result in switch result { case .success(let user): continuation.resume(returning: user) case .failure(let error): continuation.resume(throwing: error) } } } } ``` ### Continuation Resume Methods ```swift // Return a value continuation.resume(returning: value) // Throw an error continuation.resume(throwing: error) // From a Result type continuation.resume(with: result) // Result ``` ### Resume-Exactly-Once Rule A continuation MUST be resumed exactly once: - **Resuming twice** crashes with `"Continuation already resumed"` (checked) or undefined behavior (unsafe) - **Never resuming** causes the awaiting task to hang forever — a silent leak ```swift // DANGEROUS: callback might not be called func riskyBridge() async throws -> Data { try await withCheckedThrowingContinuation { continuation in api.fetch { data, error in if let error { continuation.resume(throwing: error) return } if let data { continuation.resume(returning: data) return } // BUG: if both are nil, continuation is never resumed // Fix: add a fallback continuation.resume(throwing: BridgeError.noResponse) } } } ``` ### Bridging Delegates ```swift class LocationBridge: NSObject, CLLocationManagerDelegate { private var continuation: CheckedContinuation? private let manager = CLLocationManager() func requestLocation() async throws -> CLLocation { try await withCheckedThrowingContinuation { continuation in self.continuation = continuation manager.delegate = self manager.requestLocation() } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { continuation?.resume(returning: locations[0]) continuation = nil // Prevent double resume } func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { continuation?.resume(throwing: error) continuation = nil } } ``` ### Unsafe Continuations Skip runtime checks for performance. Same API as checked, but misuse causes undefined behavior instead of a diagnostic crash. ```swift func fastBridge() async -> Data { await withUnsafeContinuation { continuation in // No runtime check for double-resume or missing resume fastCallback { data in continuation.resume(returning: data) } } } ``` **Use checked continuations during development, switch to unsafe only after thorough testing and when profiling shows the check is a bottleneck.** ### Continuation Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | Resume called twice | "Continuation already resumed" crash | Set continuation to nil after resume | | Resume never called | Task hangs indefinitely | Ensure all code paths resume — including error/nil cases | | Capturing continuation | Continuation escapes scope | Store in property, ensure single resume | | Unsafe continuation in debug | No diagnostics for misuse | Use withCheckedContinuation during development | | Delegate called multiple times | Crash on second resume | Use AsyncStream instead of continuation for repeated callbacks | | Callback on wrong thread | Doesn't matter for continuation | Continuations can be resumed from any thread | --- ## Part 8: Migration Patterns Common migrations from GCD and completion handlers to Swift concurrency. ### DispatchQueue to Actor ```swift // BEFORE: DispatchQueue for thread safety class ImageCache { private let queue = DispatchQueue(label: "cache", attributes: .concurrent) private var cache: [URL: UIImage] = [:] func get(_ url: URL, completion: @escaping (UIImage?) -> Void) { queue.async { completion(self.cache[url]) } } func set(_ url: URL, image: UIImage) { queue.async(flags: .barrier) { self.cache[url] = image } } } // AFTER: Actor actor ImageCache { private var cache: [URL: UIImage] = [:] func get(_ url: URL) -> UIImage? { cache[url] } func set(_ url: URL, image: UIImage) { cache[url] = image } } ``` ### DispatchGroup to TaskGroup ```swift // BEFORE: DispatchGroup let group = DispatchGroup() var results: [Data] = [] for url in urls { group.enter() fetch(url) { data in results.append(data) group.leave() } } group.notify(queue: .main) { use(results) } // AFTER: TaskGroup let results = await withTaskGroup(of: Data.self) { group in for url in urls { group.addTask { await fetch(url) } } var collected: [Data] = [] for await data in group { collected.append(data) } return collected } use(results) ``` ### Completion Handler to async ```swift // BEFORE func fetchData(completion: @escaping (Result) -> Void) { URLSession.shared.dataTask(with: url) { data, _, error in if let error { completion(.failure(error)); return } guard let data else { completion(.failure(FetchError.noData)); return } completion(.success(data)) }.resume() } // AFTER func fetchData() async throws -> Data { let (data, _) = try await URLSession.shared.data(from: url) return data } ``` ### @objc Delegates with @MainActor ```swift @MainActor class ViewController: UIViewController, UITableViewDelegate { // @objc delegate methods inherit @MainActor isolation from the class func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // Already on MainActor — safe to update UI updateSelection(indexPath) } } ``` ### NotificationCenter to AsyncSequence ```swift // BEFORE let observer = NotificationCenter.default.addObserver( forName: .didUpdate, object: nil, queue: .main ) { notification in handleUpdate(notification) } // Must remove observer in deinit // AFTER let task = Task { for await notification in NotificationCenter.default.notifications(named: .didUpdate) { await handleUpdate(notification) } } // Cancel task in deinit — no manual observer removal needed ``` ### Timer to AsyncSequence ```swift // BEFORE let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in updateUI() } // Must invalidate in deinit // AFTER let task = Task { while !Task.isCancelled { await updateUI() try? await Task.sleep(for: .seconds(1)) } } // Cancel task in deinit ``` ### DispatchSemaphore to Actor ```swift // BEFORE: Semaphore to limit concurrent operations let semaphore = DispatchSemaphore(value: 3) for url in urls { DispatchQueue.global().async { semaphore.wait() defer { semaphore.signal() } download(url) } } // AFTER: TaskGroup with limited concurrency await withTaskGroup(of: Void.self) { group in var inFlight = 0 for url in urls { if inFlight >= 3 { await group.next() // Wait for one to finish inFlight -= 1 } group.addTask { await download(url) } inFlight += 1 } await group.waitForAll() } ``` ### Migration Gotcha Table | Gotcha | Symptom | Fix | |---|---|---| | DispatchQueue.sync to actor | Deadlock potential | Remove .sync, use await | | Global dispatch to actor contention | Slowdown from serialization | Profile with Concurrency Instruments | | Legacy delegate + Sendable | "Cannot conform to Sendable" | Use @preconcurrency import or @MainActor isolation | | Callback called multiple times | Continuation crash | Use AsyncStream instead of continuation | | Semaphore.wait in async context | Thread starvation, potential deadlock | Use TaskGroup with manual concurrency limiting | | DispatchQueue.main.async to MainActor | Subtle timing differences | MainActor.run is the equivalent — test edge cases | | Replacing structured tasks with top-level Tasks | Losing cancellation propagation and error handling | Use async let or TaskGroup for related parallel work | | Batch @unchecked Sendable to fix warnings | Hiding real data races throughout codebase | Fix one type at a time with proper Sendable, actor, or sending | --- ## API Quick Reference | Task | API | Swift Version | |---|---|---| | Define isolated type | `actor MyActor { }` | 5.5+ | | Run on main thread | `@MainActor` | 5.5+ | | Mark as safe to share | `: Sendable` | 5.5+ | | Mark closure safe to share | `@Sendable` | 5.5+ | | Parallel tasks (fixed) | `async let` | 5.5+ | | Parallel tasks (dynamic) | `withTaskGroup` | 5.5+ | | Stream values | `AsyncStream` | 5.5+ | | Bridge callback | `withCheckedContinuation` | 5.5+ | | Check cancellation | `Task.checkCancellation()` | 5.5+ | | Task-scoped values | `@TaskLocal` | 5.5+ | | Assert isolation | `MainActor.assumeIsolated` | 5.9+ (iOS 17+) | | Capture caller isolation | `#isolation` | 6.0+ | | Lock-based sync | `Mutex` | 6.0+ (iOS 18+) | | Discard results | `withDiscardingTaskGroup` | 5.9+ (iOS 17+) | | Transfer ownership | `sending` parameter | 6.0+ | | Force background | `@concurrent` | 6.2+ | | Isolated conformance | `extension: @MainActor Proto` | 6.2+ | ## Resources **WWDC**: 2021-10132, 2021-10134, 2022-110350, 2025-268 **Docs**: /swift/concurrency, /swift/actor, /swift/sendable, /swift/taskgroup **Skills**: swift-concurrency, assume-isolated, synchronization, concurrency-profiling