| name | swift-concurrency |
| description | Resolve Swift concurrency compiler errors, adopt approachable concurrency (SE-0466), and write data-race-safe async code. Use when fixing Sendable conformance errors, actor isolation warnings, or strict concurrency diagnostics; when adopting default MainActor isolation, @concurrent, nonisolated(nonsending), or Task.immediate; when designing actor-based architectures, structured concurrency with TaskGroup, or background work offloading; or when migrating from @preconcurrency to full Swift 6 strict concurrency. |
Swift Concurrency
Review, fix, and write concurrent Swift code targeting Swift 6.3+. Apply actor
isolation, Sendable safety, and modern concurrency patterns with minimal
behavior changes.
Contents
Triage Workflow
When diagnosing a concurrency issue, follow this sequence:
Step 1: Capture context
- Copy the exact compiler diagnostic(s) and the offending symbol(s).
- Identify the project's concurrency settings:
- Swift language version (must be 6.2+).
- Whether Approachable Concurrency is enabled.
- Whether Default Actor Isolation is set to
MainActor.
- Swift 6 strict concurrency status: complete/errors in Swift 6 language mode;
Complete / Targeted / Minimal only when auditing Swift 5 migration settings.
- Determine the current actor context of the code (
@MainActor, custom actor,
nonisolated) and whether a default isolation mode is active.
- Confirm whether the code is UI-bound or intended to run off the main actor.
Step 2: Apply the smallest safe fix
Prefer edits that preserve existing behavior while satisfying data-race safety.
| Situation | Recommended fix |
|---|
| UI-bound type | Annotate the type or relevant members with @MainActor. |
| Protocol conformance on MainActor type | Use an isolated conformance: extension Foo: @MainActor Proto. |
| Global / static state | Protect with @MainActor or move into an actor. |
| Background work needed | Use a @concurrent async function on a nonisolated type. |
| Sendable error | Prefer immutable value types. Add Sendable only when correct. |
| Cross-isolation callback | Use sending parameters (SE-0430) for finer control. |
Step 3: Verify
- Rebuild and confirm the diagnostic is resolved.
- Check for new warnings introduced by the fix.
- Ensure no unnecessary
@unchecked Sendable or nonisolated(unsafe) was added.
Swift 6.2 Language Changes
Swift 6.2 introduces "approachable concurrency" -- a set of language changes
that make concurrent code safer by default while reducing annotation burden.
In Xcode, Approachable Concurrency and Default Actor Isolation are separate
build settings: use Approachable Concurrency for the bundled upcoming-feature
flags, and set Default Actor Isolation to MainActor when you want unannotated
code inferred as @MainActor.
SE-0466: Default MainActor Isolation
With the -default-isolation MainActor compiler flag, SwiftPM
.defaultIsolation(MainActor.self), or Xcode's Default Actor Isolation
setting set to MainActor, unannotated declarations in the module are inferred
as @MainActor unless explicitly opted out.
Effect: Eliminates most data-race safety errors for UI-bound code and
global/static state without writing @MainActor everywhere.
final class StickerLibrary {
static let shared = StickerLibrary()
var stickers: [Sticker] = []
}
final class StickerModel {
let photoProcessor = PhotoProcessor()
var selection: [PhotosPickerItem] = []
}
extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
When to use: Recommended for apps, scripts, and other executable targets
where most code is UI-bound. Not recommended for library targets that should
remain actor-agnostic.
SE-0461: nonisolated(nonsending)
Nonisolated async functions now stay on the caller's actor by default instead
of hopping to the global concurrent executor. This is the
nonisolated(nonsending) behavior.
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? {
}
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}
Use @concurrent to explicitly request background execution when needed.
@concurrent Attribute
@concurrent ensures a function always runs on the concurrent thread pool,
freeing the calling actor to run other tasks.
class PhotoProcessor {
var cachedStickers: [String: Sticker] = [:]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] { return sticker }
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
@concurrent
static func extractSubject(from data: Data) async -> Sticker {
}
}
To move a function to a background thread:
- Ensure the containing type is
nonisolated (or the function itself is).
- Add
@concurrent to the function.
- Add
async if not already asynchronous.
- Add
await at call sites.
nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { }
}
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
SE-0472: Task.immediate
Task.immediate starts executing synchronously on the current actor before
any suspension point, rather than being enqueued.
Task.immediate { await handleUserInput() }
Use for latency-sensitive work that should begin without delay. There is also
Task.immediateDetached which combines immediate start with detached semantics.
SE-0475: Transactional Observation (Observations)
Observations { } provides async observation of @Observable types via
AsyncSequence, enabling transactional change tracking.
for await _ in Observations { model.count } {
print("Count changed to \(model.count)")
}
Isolated Conformances
A conformance that needs MainActor state is called an isolated conformance.
The compiler ensures it is only used in a matching isolation context.
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
}
If ImageExporter were nonisolated, adding a StickerModel would fail:
"Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be
used in nonisolated context."
Clock Epochs
ContinuousClock and SuspendingClock now expose .epoch (SE-0473), enabling instant comparison and conversion between clock types.
let continuous = ContinuousClock()
let elapsed = continuous.now - continuous.epoch
Actor Isolation Rules
- All mutable shared state MUST be protected by an actor or global actor.
@MainActor for all UI-touching code. No exceptions.
- Use
nonisolated only for methods that access immutable (let) properties
or are pure computations.
- Use
@concurrent to explicitly move work off the caller's actor.
- Never use
nonisolated(unsafe) unless you have proven internal
synchronization and exhausted all other options.
- Never add manual locks (
NSLock, DispatchSemaphore) inside actors.
Sendable Rules
- Value types (structs, enums) are automatically
Sendable when all stored
properties are Sendable.
- Actors are implicitly
Sendable.
@MainActor classes are implicitly Sendable. Do NOT add redundant
Sendable conformance.
- Non-actor classes: must be
final with all stored properties let and
Sendable.
@unchecked Sendable is a last resort. Document why the compiler cannot
prove safety.
- Use
sending parameters (SE-0430) for finer-grained isolation control.
- Use
@preconcurrency import only for third-party libraries you cannot
modify. Plan to remove it.
Structured Concurrency Patterns
Async Defer
defer blocks can now contain await (SE-0493). Use for async cleanup — closing connections, flushing buffers, or releasing resources that require an async call.
func fetchData() async throws -> Data {
let connection = try await openConnection()
defer { await connection.close() }
return try await connection.read()
}
Task: Unstructured, inherits caller context.
Task { await doWork() }
Task.detached: No inherited context. Use only when you explicitly need to
break isolation inheritance.
Task.immediate: Starts immediately on current actor. Use for
latency-sensitive work.
Task.immediate { await handleUserInput() }
async let: Fixed number of concurrent operations.
async let a = fetchA()
async let b = fetchB()
let result = try await (a, b)
TaskGroup: Dynamic number of concurrent operations.
try await withThrowingTaskGroup(of: Item.self) { group in
for id in ids {
group.addTask { try await fetch(id) }
}
for try await item in group { process(item) }
}
Task Cancellation
- Cancellation is cooperative. Check
Task.isCancelled or call
try Task.checkCancellation() in loops.
- Use
.task modifier in SwiftUI -- it handles cancellation on view disappear.
- Use
withTaskCancellationHandler for cleanup.
- Cancel stored tasks in
deinit or onDisappear.
Actor Reentrancy
Actors are reentrant. State can change across suspension points.
actor Counter {
var count = 0
func increment() async {
let current = count
await someWork()
count = current + 1
}
}
actor Counter {
var count = 0
func increment() { count += 1 }
}
AsyncSequence and AsyncStream
Use AsyncStream to bridge callback/delegate APIs:
let stream = AsyncStream<Location> { continuation in
let delegate = LocationDelegate { location in
continuation.yield(location)
}
continuation.onTermination = { _ in delegate.stop() }
delegate.start()
}
Use withCheckedContinuation / withCheckedThrowingContinuation for
single-value callbacks. Resume exactly once.
@Observable and Concurrency
@Observable classes should be @MainActor for view models.
- Use
@State to own an @Observable instance (replaces @StateObject).
- Use
Observations { } (SE-0475) for async observation of @Observable
properties as an AsyncSequence.
Synchronization Primitives
When actors are not the right fit — synchronous access, performance-critical
paths, or bridging C/ObjC — use low-level synchronization primitives:
Mutex<Value> (iOS 18+, Synchronization module): Preferred lock for
new code. Stores protected state inside the lock. withLock { } pattern.
OSAllocatedUnfairLock (iOS 16+, os module): Use when targeting
older iOS versions. Supports ownership assertions for debugging.
Atomic<Value> (iOS 18+, Synchronization module): Lock-free atomics
for simple counters and flags. Requires explicit memory ordering.
Key rule: Never put locks inside actors (double synchronization), and never
hold a lock across await (deadlock risk). See
references/synchronization-primitives.md for full API details, code examples,
and a decision guide for choosing locks vs actors.
Common Mistakes
- Blocking the main actor. Heavy computation on
@MainActor freezes UI.
Move to a @concurrent function.
- Unnecessary @MainActor. Network layers, data processing, and model code
do not need
@MainActor. Only UI-touching code does.
- Actors for stateless code. No mutable state means no actor needed. Use a
plain struct or function.
- Actors for immutable data. Use a
Sendable struct, not an actor.
- Task.detached without good reason. Loses priority, task-local values,
and cancellation propagation.
- Forgetting task cancellation. Store
Task references and cancel them, or
use the .task view modifier.
- Retain cycles in Tasks. Use
[weak self] when capturing self in
long-lived stored tasks.
- Semaphores in async context.
DispatchSemaphore.wait() in async code
will deadlock. Use structured concurrency instead.
- Split isolation. Mixing
@MainActor and nonisolated properties in one
type. Isolate the entire type consistently.
- MainActor.run instead of static isolation. Prefer
@MainActor func
over await MainActor.run { }.
- Using GCD APIs. Never use DispatchQueue, DispatchGroup, DispatchSemaphore, or any GCD API. Use async/await, actors, and TaskGroups instead. GCD has no data-race safety guarantees.
Review Checklist
References