Files
Matthias a60a76b797 Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
2026-04-19 21:11:32 +02:00

1399 lines
39 KiB
Markdown

---
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<T> {
let value: T
}
// Box is Sendable only when T is Sendable
extension Box: Sendable where T: Sendable {}
// Standard library uses this extensively:
// Array<Element>: Sendable where Element: Sendable
// Dictionary<Key, Value>: Sendable where Key: Sendable, Value: Sendable
// Optional<Wrapped>: 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<Success, Failure>
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<T: Sendable>(
_ 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<Void> {
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<Int> { 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<Data, Error> { 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<Value> { 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<Value, Error> { 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<Int>(bufferingPolicy: .unbounded) { continuation in
// ...
}
// Keep oldest N values, drop new ones when buffer is full
let stream = AsyncStream<Int>(bufferingPolicy: .bufferingOldest(100)) { continuation in
// ...
}
// Keep newest N values, drop old ones when buffer is full
let stream = AsyncStream<Int>(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<T, Error>
```
### 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<CLLocation, Error>?
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<Data, Error>) -> 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