Swift-ios-skills swift-concurrency
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.
git clone https://github.com/dpearson2699/swift-ios-skills
T=$(mktemp -d) && git clone --depth=1 https://github.com/dpearson2699/swift-ios-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/swift-concurrency" ~/.claude/skills/dpearson2699-swift-ios-skills-swift-concurrency && rm -rf "$T"
skills/swift-concurrency/SKILL.mdSwift 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
- Swift 6.2 Language Changes
- Actor Isolation Rules
- Sendable Rules
- Structured Concurrency Patterns
- Task Cancellation
- Actor Reentrancy
- AsyncSequence and AsyncStream
- @Observable and Concurrency
- Synchronization Primitives
- Common Mistakes
- Review Checklist
- References
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 (default MainActor isolation) is enabled.
- Strict concurrency checking level (Complete / Targeted / Minimal).
- Determine the current actor context of the code (
, custom@MainActor
,actor
) and whether a default isolation mode is active.nonisolated - 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 . |
| Protocol conformance on MainActor type | Use an isolated conformance: . |
| Global / static state | Protect with or move into an actor. |
| Background work needed | Use a async function on a type. |
| Sendable error | Prefer immutable value types. Add only when correct. |
| Cross-isolation callback | Use 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
or@unchecked Sendable
was added.nonisolated(unsafe)
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.
SE-0466: Default MainActor Isolation
With the
-default-isolation MainActor compiler flag (or the Xcode 26
"Approachable Concurrency" build setting), all code in a module runs on
@MainActor by default unless explicitly opted out.
Effect: Eliminates most data-race safety errors for UI-bound code and global/static state without writing
@MainActor everywhere.
// With default MainActor isolation enabled, these are implicitly @MainActor: final class StickerLibrary { static let shared = StickerLibrary() // safe -- on MainActor var stickers: [Sticker] = [] } final class StickerModel { let photoProcessor = PhotoProcessor() var selection: [PhotosPickerItem] = [] } // Conformances are also implicitly isolated: 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? { // In Swift 6.2+, this runs on the caller's actor (e.g., MainActor) // instead of hopping to a background thread. // ... } } @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 } // No data race -- photoProcessor stays on MainActor 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 { // Expensive image processing -- runs on background thread pool // ... } }
To move a function to a background thread:
- Ensure the containing type is
(or the function itself is).nonisolated - Add
to the function.@concurrent - Add
if not already asynchronous.async - Add
at call sites.await
nonisolated struct PhotoProcessor { @concurrent func process(data: Data) async -> ProcessedPhoto? { /* ... */ } } // Caller: 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() } // Isolated conformance: only usable on MainActor extension StickerModel: @MainActor Exportable { func export() { photoProcessor.exportAsPNG() } } @MainActor struct ImageExporter { var items: [any Exportable] mutating func add(_ item: StickerModel) { items.append(item) // OK -- ImageExporter is on MainActor } }
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 // Duration since system boot
Actor Isolation Rules
- All mutable shared state MUST be protected by an actor or global actor.
for all UI-touching code. No exceptions.@MainActor- Use
only for methods that access immutable (nonisolated
) properties or are pure computations.let - Use
to explicitly move work off the caller's actor.@concurrent - Never use
unless you have proven internal synchronization and exhausted all other options.nonisolated(unsafe) - Never add manual locks (
,NSLock
) inside actors.DispatchSemaphore
Sendable Rules
- Value types (structs, enums) are automatically
when all stored properties areSendable
.Sendable - Actors are implicitly
.Sendable
classes are implicitly@MainActor
. Do NOT add redundantSendable
conformance.Sendable- Non-actor classes: must be
with all stored propertiesfinal
andlet
.Sendable
is a last resort. Document why the compiler cannot prove safety.@unchecked Sendable- Use
parameters (SE-0430) for finer-grained isolation control.sending - Use
only for third-party libraries you cannot modify. Plan to remove it.@preconcurrency import
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
or callTask.isCancelled
in loops.try Task.checkCancellation() - Use
modifier in SwiftUI -- it handles cancellation on view disappear..task - Use
for cleanup.withTaskCancellationHandler - Cancel stored tasks in
ordeinit
.onDisappear
Actor Reentrancy
Actors are reentrant. State can change across suspension points.
// WRONG: State may change during await actor Counter { var count = 0 func increment() async { let current = count await someWork() count = current + 1 // BUG: count may have changed } } // CORRECT: Mutate synchronously, no reentrancy risk 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
classes should be@Observable
for view models.@MainActor- Use
to own an@State
instance (replaces@Observable
).@StateObject - Use
(SE-0475) for async observation ofObservations { }
properties as an@Observable
.AsyncSequence
Synchronization Primitives
When actors are not the right fit — synchronous access, performance-critical paths, or bridging C/ObjC — use low-level synchronization primitives:
(iOS 18+,Mutex<Value>
module): Preferred lock for new code. Stores protected state inside the lock.Synchronization
pattern.withLock { }
(iOS 16+,OSAllocatedUnfairLock
module): Use when targeting older iOS versions. Supports ownership assertions for debugging.os
(iOS 18+,Atomic<Value>
module): Lock-free atomics for simple counters and flags. Requires explicit memory ordering.Synchronization
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
freezes UI. Move to a@MainActor
function.@concurrent - Unnecessary @MainActor. Network layers, data processing, and model code
do not need
. Only UI-touching code does.@MainActor - Actors for stateless code. No mutable state means no actor needed. Use a plain struct or function.
- Actors for immutable data. Use a
struct, not an actor.Sendable - Task.detached without good reason. Loses priority, task-local values, and cancellation propagation.
- Forgetting task cancellation. Store
references and cancel them, or use theTask
view modifier..task - Retain cycles in Tasks. Use
when capturing[weak self]
in long-lived stored tasks.self - Semaphores in async context.
in async code will deadlock. Use structured concurrency instead.DispatchSemaphore.wait() - Split isolation. Mixing
and@MainActor
properties in one type. Isolate the entire type consistently.nonisolated - MainActor.run instead of static isolation. Prefer
over@MainActor func
.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
- All mutable shared state is actor-isolated
- No data races (no unprotected cross-isolation access)
- Tasks are cancelled when no longer needed
- No blocking calls on
@MainActor - No manual locks inside actors
-
conformance is correct (no unjustifiedSendable
)@unchecked - Actor reentrancy is handled (no state assumptions across awaits)
-
imports are documented with removal plan@preconcurrency - Heavy work uses
, not@concurrent@MainActor -
modifier used in SwiftUI instead of manual Task management.task
References
- See references/concurrency-patterns.md for detailed approachable concurrency patterns, patterns, and migration examples.
- See references/approachable-concurrency.md for the approachable concurrency mode quick-reference guide.
- See references/swiftui-concurrency.md for SwiftUI-specific concurrency guidance.
- See references/synchronization-primitives.md for Mutex, OSAllocatedUnfairLock, and guidance on choosing locks vs actors.