How to install swift-concurrency
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-concurrencyFull instructions (SKILL.md)
Source of truth, from dpearson2699/swift-ios-skills.
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+. Gate Swift 6.4 / Xcode 27 beta cleanup APIs behind explicit toolchain and availability checks. 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+).
- Xcode/toolchain version for version-specific features and release-note workarounds.
- 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, customactor,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 Sendableornonisolated(unsafe)was added. - For build-setting reviews, stop at settings plus the smallest code-level remediation. Do not add Thread Sanitizer, broad migration ordering, or architecture advice unless the prompt asks for diagnostics or migration.
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.
// 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, show both opt-outs together:
- Ensure the containing type is
nonisolatedor the function can be called from a nonisolated context. - Add
@concurrentto the offloaded function.nonisolatedalone does not move CPU-heavy work off the caller's actor. - Add
asyncif not already asynchronous. - Add
awaitat call sites.
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.
@MainActorfor all UI-touching code. No exceptions. Global actors are actor isolation; use@MainActoras the standard pattern for UI-bound shared state.- Use
nonisolatedonly for methods that access immutable (let) properties or are pure computations. - Use
@concurrentto explicitly move work off the caller's actor. - Never use
nonisolated(unsafe)unless you have proven internal synchronization and exhausted all other options. It is an unsafe audit boundary, not a synchronization primitive. - Never add manual locks (
NSLock,DispatchSemaphore) inside actors.
Sendable Rules
- Value types (structs, enums) are automatically
Sendablewhen all stored properties areSendable. For diagnostics on mutable reference types, first extract an immutableSendablevalue snapshot or DTO instead of sharing the reference. - Actors are implicitly
Sendable. @MainActorclasses are implicitlySendable. Do NOT add redundantSendableconformance.- Non-actor classes: must be
finalwith all stored propertiesletandSendable. @unchecked Sendableis a last resort. Document why the compiler cannot prove safety.- Use
sendingparameters (SE-0430) for finer-grained isolation control. - Use
@preconcurrency importonly for third-party libraries you cannot modify. Plan to remove it.
Structured Concurrency Patterns
Async Defer (Swift 6.4+)
defer blocks in async contexts can contain await in Swift 6.4+ (SE-0493).
Use for async cleanup: closing connections, flushing buffers, or releasing
resources that require an async call.
The defer body inherits the surrounding isolation and is implicitly awaited at
scope exit. It does not suppress cancellation; cleanup that checks
Task.isCancelled or Task.checkCancellation() still observes cancellation.
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.isCancelledor calltry Task.checkCancellation()in loops. - Use
.taskmodifier in SwiftUI -- it handles cancellation on view disappear. - Use
withTaskCancellationHandlerfor cleanup. - Swift 6.4 / iOS 27+ beta: use
withTaskCancellationShieldonly for short cleanup or rollback that must complete after cancellation. Inside the shield,Task.isCancelledis false andTask.checkCancellation()does not throw; cancellation is observable again after the scope exits. - Cancel stored tasks in
deinitoronDisappear.
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
@Observableclasses should be@MainActorfor view models.- Use
@Stateto own an@Observableinstance (replaces@StateObject). - Use
Observations { }(SE-0475) for async observation of@Observableproperties as anAsyncSequence.
Synchronization Primitives
When actors are not the right fit — synchronous access, performance-critical paths, or bridging C/ObjC — use low-level synchronization primitives:
- Actors remain the default for async shared state when callers can suspend;
they give compiler-enforced isolation and structured-concurrency integration,
but outside calls are async actor hops, require reentrancy care across
await, and do not fit synchronous C callbacks. Use global actors such as@MainActorfor UI-bound shared state; never usenonisolated(unsafe)as a synchronization substitute. Mutex<Value>(iOS 18+,Synchronizationmodule): Preferred lock for new code. Stores protected state inside the lock.withLock { }pattern.OSAllocatedUnfairLock(iOS 16+,osmodule): Use when targeting older iOS versions. Supports ownership assertions for debugging.Atomic<Value>(iOS 18+,Synchronizationmodule): Lock-free atomics for independent counters and flags.AtomicisSendableand can be stored inSendableholder types. Use.relaxedonly for standalone metrics; use acquire/release ordering or a lock when coordinating other data.
Key rule: Never put locks inside actors (double synchronization), and never
hold a lock across await (blocks a thread through suspension and can starve
the cooperative pool or deadlock). See
references/synchronization-primitives.md for full API details, code examples,
and a decision guide for choosing locks vs actors.
Mutex.withLock and OSAllocatedUnfairLock.withLock use synchronous closures;
that API shape is what keeps critical sections non-suspending.
Gate Mutex and Atomic with runtime if #available(iOS 18, *), never
#if swift(...) or platform compile-time checks. For NSLock, correct only the
false Sendable premise and avoid explaining conformance mechanics.
If a legacy lock wrapper truly needs @unchecked Sendable, name the invariant:
all mutable state is private, all access uses one lock, no mutable references
escape, and no lock is held across await.
Common Mistakes
- Blocking the main actor. Heavy computation on
@MainActorfreezes UI. Move to a@concurrentfunction. - 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
Sendablestruct, not an actor. - Task.detached without good reason. Loses priority, task-local values, and cancellation propagation.
- Forgetting task cancellation. Store
Taskreferences and cancel them, or use the.taskview modifier. - Retain cycles in Tasks. Use
[weak self]when capturingselfin long-lived stored tasks. - Semaphores in async context.
DispatchSemaphore.wait()in async code will deadlock. Use structured concurrency instead. - Split isolation. Mixing
@MainActorandnonisolatedproperties in one type. Isolate the entire type consistently. - MainActor.run instead of static isolation. Prefer
@MainActor funcoverawait 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
-
Sendableconformance is correct (no unjustified@unchecked) - Actor reentrancy is handled (no state assumptions across awaits)
-
@preconcurrencyimports are documented with removal plan - Heavy work uses
@concurrent, not@MainActor -
.taskmodifier used in SwiftUI instead of manual Task management
References
- references/concurrency-patterns.md — detailed concurrency patterns and migration examples
- references/approachable-concurrency.md — approachable concurrency mode quick-reference
- references/swiftui-concurrency.md — SwiftUI-specific concurrency guidance
- references/synchronization-primitives.md — Mutex, OSAllocatedUnfairLock, locks vs actors
- references/bridging-interop.md — checked continuations, delegate bridging, GCD migration table
- references/diagnostics.md — compiler diagnostic → fix reference, strict concurrency adoption
- references/async-algorithms.md — swift-async-algorithms: debounce, throttle, merge, combineLatest, chunks
Related skills
More from dpearson2699/swift-ios-skills and the wider catalog.
swiftui-animation
Implement, review, or improve SwiftUI animations and transitions. Use when adding explicit animations with withAnimation, configuring implicit animations with .animation(_:body:) or .animation(_:value:), configuring spring animations (.smooth, .snappy, .bouncy), building phase or keyframe animations with PhaseAnimator/KeyframeAnimator, creating hero transitions with matchedGeometryEffect or matchedTransitionSource, adding SF Symbol effects (iOS 17 bounce, pulse, variableColor, scale, appear, disappear, replace; iOS 18 breathe, rotate, wiggle), implementing custom Transition or CustomAnimation types, or ensuring animations respect accessibilityReduceMotion.
ios-accessibility
Implements, reviews, or improves accessibility in iOS/macOS apps with SwiftUI, UIKit, and AppKit. Use when adding VoiceOver, Voice Control, Switch Control, or Full Keyboard Access support; when working with accessibility labels, hints, values, traits, accessibilityInputLabels, NSAccessibility, grouping, reading order, accessibility focus restoration with @AccessibilityFocusState, Dynamic Type, @ScaledMetric, custom rotors, accessibility actions, XCTest accessibility checks, App Store Accessibility Nutrition Labels, App Store Connect accessibility answers, a11y compliance audits, or system accessibility preferences.
swiftui-patterns
Builds and reviews SwiftUI views with modern MV architecture, state management, view composition, and migration/availability guidance. Covers @Observable ownership rules, @State/@Bindable/@Environment wiring, view decomposition, custom ViewModifiers, environment values, async data loading with .task, iOS 26+ handoff reminders, Writing Tools, clipboard availability caveats, and performance guidelines. Use when structuring SwiftUI app state, managing @Observable, composing view hierarchies, or correcting SwiftUI pattern guidance.
swiftui-performance
Audit and improve SwiftUI runtime performance. Use when diagnosing slow rendering, janky scrolling, high CPU, memory usage, excessive view updates, layout thrash, body evaluation cost, identity churn, view lifetime issues, lazy loading, Instruments profiling guidance, and performance audit requests.
app-store-review
Prepare for App Store review and prevent rejections. Covers App Store review guidelines, app rejection reasons, PrivacyInfo.xcprivacy privacy manifest requirements, required API reason codes, in-app purchase IAP and StoreKit rules, App Store Guidelines compliance, ATT App Tracking Transparency, EU DMA Digital Markets Act, HIG compliance checklist, app submission preparation, review preparation, metadata requirements, entitlements, widgets, and Live Activities review rules. Use when preparing for App Store submission, fixing rejection reasons, auditing privacy manifests, implementing ATT consent flow, configuring StoreKit IAP, or checking HIG compliance.
ios-networking
Build, review, or improve networking code in iOS/macOS apps using URLSession with async/await, structured concurrency, and modern Swift patterns. Use when working with REST APIs, downloading files, uploading data, WebSocket connections, pagination, retry logic, request middleware, caching, background transfers, or network reachability monitoring. Also use when handling HTTP requests, API clients, network error handling, or data fetching in Swift apps.