Help us improve
Share bugs, ideas, or general feedback.
From apple-kit-skills
Fixes Swift concurrency compiler errors, adopts approachable concurrency (SE-0466), and writes data-race-safe async code. Use when resolving Sendable conformance errors, actor isolation warnings, or migrating to Swift 6 strict concurrency.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin swiftui-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/apple-kit-skills:swift-concurrencyThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Review, fix, and write concurrent Swift code targeting Swift 6.3+. Apply actor
Diagnoses Swift Concurrency issues, refactors callback code to async/await, and guides Swift 6 migration for tasks, actors, Sendable, and data races.
Reviews and fixes Swift concurrency issues like actor isolation and Sendable violations in Swift 6.2+ codebases. Useful for compiler diagnostics, async migration, and data-race safety.
Guides building, auditing, and refactoring Swift concurrency code with async/await, actors, MainActor, Tasks, and structured patterns in Swift 6+.
Share bugs, ideas, or general feedback.
Review, fix, and write concurrent Swift code targeting Swift 6.3+. Apply actor isolation, Sendable safety, and modern concurrency patterns with minimal behavior changes.
@Observable and ConcurrencyWhen diagnosing a concurrency issue, follow this sequence:
@MainActor, custom actor,
nonisolated) and whether a default isolation mode is active.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. |
@unchecked Sendable or nonisolated(unsafe) was added.Swift 6.2 introduces "approachable concurrency" -- a set of language changes that make concurrent code safer by default while reducing annotation burden.
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.
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:
nonisolated (or the function itself is).@concurrent to the function.async if not already asynchronous.await at call sites.nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { /* ... */ }
}
// Caller:
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
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.
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)")
}
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."
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
@MainActor for all UI-touching code. No exceptions.nonisolated only for methods that access immutable (let) properties
or are pure computations.@concurrent to explicitly move work off the caller's actor.nonisolated(unsafe) unless you have proven internal
synchronization and exhausted all other options.NSLock, DispatchSemaphore) inside actors.Sendable when all stored
properties are Sendable.Sendable.@MainActor classes are implicitly Sendable. Do NOT add redundant
Sendable conformance.final with all stored properties let and
Sendable.@unchecked Sendable is a last resort. Document why the compiler cannot
prove safety.sending parameters (SE-0430) for finer-grained isolation control.@preconcurrency import only for third-party libraries you cannot
modify. Plan to remove it.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.isCancelled or call
try Task.checkCancellation() in loops..task modifier in SwiftUI -- it handles cancellation on view disappear.withTaskCancellationHandler for cleanup.deinit or onDisappear.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 }
}
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.@State to own an @Observable instance (replaces @StateObject).Observations { } (SE-0475) for async observation of @Observable
properties as an AsyncSequence.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.
@MainActor freezes UI.
Move to a @concurrent function.@MainActor. Only UI-touching code does.Sendable struct, not an actor.Task references and cancel them, or
use the .task view modifier.[weak self] when capturing self in
long-lived stored tasks.DispatchSemaphore.wait() in async code
will deadlock. Use structured concurrency instead.@MainActor and nonisolated properties in one
type. Isolate the entire type consistently.@MainActor func
over await MainActor.run { }.@MainActorSendable conformance is correct (no unjustified @unchecked)@preconcurrency imports are documented with removal plan@concurrent, not @MainActor.task modifier used in SwiftUI instead of manual Task management