Swift Concurrency
avdlee/swift-concurrency-agent-skillThis skill provides guidance on implementing and resolving issues related to Swift Concurrency, focusing on safe and effective use of async/await, actors, and structured concurrency in Swift applications. It offers key capabilities such as analyzing project settings, identifying isolation boundaries, recommending minimal and safe fixes, and escalating issues when necessary, all tailored to different development scenarios. This is ideal for Swift developers seeking to enhance concurrency safety, optimize performance, and ensure correct asynchronous behavior in their code.
Swift Concurrency
Agent Rules
- Analyze
Package.swiftor.pbxprojto determine Swift language mode (5.x vs 6) and toolchain before giving advice. - Before proposing fixes, identify the isolation boundary:
@MainActor, custom actor, actor instance isolation, or nonisolated. - Do not recommend
@MainActoras a blanket fix. Justify why main-actor isolation is correct for the code. - Prefer structured concurrency (child tasks, task groups) over unstructured tasks. Use
Task.detachedonly with a clear reason. - If recommending
@preconcurrency,@unchecked Sendable, ornonisolated(unsafe), require:- a documented safety invariant
- a follow-up ticket to remove or migrate it
- For migration work, optimize for minimal blast radius (small, reviewable changes) and follow the validation loop: Build → Fix errors → Rebuild → Only proceed when clean.
- Course references are for deeper learning only. Use them sparingly and only when they clearly help answer the developer's question.
Triage Checklist (Before Advising)
- Capture the exact compiler diagnostics and the offending symbol(s).
- Identify the current isolation boundary and module defaults (
@MainActor, custom actor, default isolation). - Confirm whether the code is UI-bound or intended to run off the main actor.
Quick Fix Mode (Use When)
Use Quick Fix Mode when:
- The errors are localized (single file or one type) and the isolation boundary is clear.
- The fix does not require API redesign or multi-module changes.
- You can explain the fix in 1–2 steps without changing behavior. Skip Quick Fix Mode when:
- Default isolation or strict concurrency settings are unknown and likely affect behavior.
- The error crosses module boundaries or involves public API changes.
- The fix would require
@unchecked Sendable,@preconcurrency, ornonisolated(unsafe)without a clear invariant.
Project Settings Intake (Evaluate Before Advising)
Concurrency behavior depends on build settings. Before advising, determine these via Read on Package.swift or Grep in .pbxproj files:
Setting
SwiftPM (Package.swift)
Xcode (.pbxproj)
Default isolation
.defaultIsolation(MainActor.self)
SWIFT_DEFAULT_ACTOR_ISOLATION
Strict concurrency
.enableExperimentalFeature("StrictConcurrency=targeted")
SWIFT_STRICT_CONCURRENCY
Upcoming features
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
SWIFT_UPCOMING_FEATURE_*
Language mode
// swift-tools-version: at top
Swift Language Version build setting
If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance.
Smallest Safe Fixes (Quick Wins)
Prefer edits that preserve behavior while satisfying data-race safety.
- UI-bound types: isolate the type or specific members to
@MainActor(justify why UI-bound). - Global/static mutable state: move into an
actoror isolate to@MainActorif UI-only. - Background work: for work that should always hop off the caller’s isolation, move expensive work into an
asyncfunction marked@concurrent; for work that doesn’t touch isolated state but can inherit the caller’s isolation (for example withNonisolatedNonsendingByDefault), usenonisolatedwithout@concurrent, or use anactorto guard mutable state. - Sendable errors: prefer immutable/value types; avoid
@unchecked Sendableunless you can prove and document thread safety.
Quick Fix Playbook (Common Diagnostics -> Minimal Fix)
- "Main actor-isolated ... cannot be used from a nonisolated context"
- Quick fix: if UI-bound, make the caller
@MainActoror hop withawait MainActor.run { ... }. - Escalate if this is non-UI code or causes reentrancy; use
references/actors.md.
- Quick fix: if UI-bound, make the caller
- "Actor-isolated type does not conform to protocol"
- Quick fix: add isolated conformance (e.g.,
extension Foo: @MainActor SomeProtocol). - Escalate if the protocol requirements must be
nonisolated; usereferences/actors.md.
- Quick fix: add isolated conformance (e.g.,
- "Sending value of non-Sendable type ... risks causing data races"
- Quick fix: confine access inside an actor or convert to a value type with immutable (
let) state. - Escalate before
@unchecked Sendable; usereferences/sendable.mdandreferences/threading.md.
- Quick fix: confine access inside an actor or convert to a value type with immutable (
- SwiftLint
async_without_await- Quick fix: remove
asyncif not required; if required by protocol/override/@concurrent, use narrow suppression with rationale. Seereferences/linting.md.
- Quick fix: remove
- "wait(...) is unavailable from asynchronous contexts" (XCTest)
- Quick fix: use
await fulfillment(of:)or Swift Testing equivalents. Seereferences/testing.md.
- Quick fix: use
Escalation Path (When Quick Fixes Aren't Enough)
- Gather project settings (default isolation, strict concurrency level, upcoming features).
- Re-evaluate isolation boundaries and which types cross them.
- Use the decision tree + references for the deeper fix.
- If behavior changes are possible, document the invariant and add tests/verification steps.
Quick Decision Tree
When a developer needs concurrency guidance, follow this decision tree:
- Starting fresh with async code?
- Read
references/async-await-basics.mdfor foundational patterns - For parallel operations →
references/tasks.md(async let, task groups)
- Read
- Protecting shared mutable state?
- Need to protect class-based state →
references/actors.md(actors, @MainActor) - Need thread-safe value passing →
references/sendable.md(Sendable conformance)
- Need to protect class-based state →
- Managing async operations?
- Structured async work →
references/tasks.md(Task, child tasks, cancellation) - Streaming data →
references/async-sequences.md(AsyncSequence, AsyncStream)
- Structured async work →
- Working with legacy frameworks?
- Core Data integration →
references/core-data.md - General migration →
references/migration.md
- Core Data integration →
- Performance or debugging issues?
- Slow async code →
references/performance.md(profiling, suspension points) - Testing concerns →
references/testing.md(XCTest, Swift Testing)
- Slow async code →
- Understanding threading behavior?
- Read
references/threading.mdfor thread/task relationship and isolation
- Read
- Memory issues with tasks?
- Read
references/memory-management.mdfor retain cycle prevention
- Read
Triage-First Playbook (Common Errors -> Next Best Move)
- SwiftLint concurrency-related warnings
- Use
references/linting.mdfor rule intent and preferred fixes; avoid dummy awaits as “fixes”.
- Use
- SwiftLint
async_without_awaitwarning- Remove
asyncif not required; if required by protocol/override/@concurrent, prefer narrow suppression over adding fake awaits. Seereferences/linting.md.
- Remove
- "Sending value of non-Sendable type ... risks causing data races"
- First: identify where the value crosses an isolation boundary
- Then: use
references/sendable.mdandreferences/threading.md(especially Swift 6.2 behavior changes)
- "Main actor-isolated ... cannot be used from a nonisolated context"
- First: decide if it truly belongs on
@MainActor - Then: use
references/actors.md(global actors,nonisolated, isolated parameters) andreferences/threading.md(default isolation)
- First: decide if it truly belongs on
- "Class property 'current' is unavailable from asynchronous contexts" (Thread APIs)
- Use
references/threading.mdto avoid thread-centric debugging and rely on isolation + Instruments
- Use
- "Actor-isolated type does not conform to protocol" (protocol conformance errors)
- First: determine whether the protocol requirements must execute on the actor (for example, UI work on
@MainActor) or can safely benonisolated. - Then: follow the Quick Fix Playbook entry for actor-isolated protocol conformance and
references/actors.mdfor implementation patterns (isolated conformances,nonisolatedrequirements, and escalation steps).
- First: determine whether the protocol requirements must execute on the actor (for example, UI work on
- XCTest async errors like "wait(...) is unavailable from asynchronous contexts"
- Use
references/testing.md(await fulfillment(of:)and Swift Testing patterns)
- Use
- Core Data concurrency warnings/errors
- Use
references/core-data.md(DAO/NSManagedObjectID, default isolation conflicts)
- Use
Core Patterns Reference
Concurrency Tool Selection
Need
Tool
Key Guidance
Single async operation
async/await
Default choice for sequential async work
Fixed parallel operations
async let
Known count at compile time; auto-cancelled on throw
Dynamic parallel operations
withTaskGroup
Unknown count; structured — cancels children on scope exit
Sync → async bridge
Task { }
Inherits actor context; use Task.detached only with documented reason
Shared mutable state
actor
Prefer over locks/queues; keep isolated sections small
UI-bound state
@MainActor
Only for truly UI-related code; justify isolation
Common Scenarios
Network request with UI update
Task { @concurrent in
let data = try await fetchData()
await MainActor.run { self.updateUI(with: data) }
}
Processing array items in parallel
await withTaskGroup(of: ProcessedItem.self) { group in
for item in items {
group.addTask { await process(item) }
}
for await result in group {
results.append(result)
}
}
Swift 6 Migration Quick Guide
Key changes in Swift 6:
- Strict concurrency checking enabled by default
- Complete data-race safety at compile time
- Sendable requirements enforced on boundaries
- Isolation checking for all async boundaries
Migration Validation Loop
Apply this cycle for each migration change:
- Build — Run
swift buildor Xcode build to surface new diagnostics - Fix — Address one category of error at a time (e.g., all Sendable issues first)
- Rebuild — Confirm the fix compiles cleanly before moving on
- Test — Run the test suite to catch regressions (
swift testor Cmd+U) - Only proceed to the next file/module when all diagnostics are resolved
If a fix introduces new warnings, resolve them before continuing. Never batch multiple unrelated fixes — keep commits small and reviewable.
For detailed migration steps, see
references/migration.md.
Reference Files
Load these files as needed for specific topics:
async-await-basics.md- async/await syntax, execution order, async let, URLSession patternstasks.md- Task lifecycle, cancellation, priorities, task groups, structured vs unstructuredthreading.md- Thread/task relationship, suspension points, isolation domains, nonisolatedmemory-management.md- Retain cycles in tasks, memory safety patternsactors.md- Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutexsendable.md- Sendable conformance, value/reference types, @unchecked, region isolationlinting.md- Concurrency-focused lint rules and SwiftLintasync_without_awaitasync-sequences.md- AsyncSequence, AsyncStream, when to use vs regular async methodscore-data.md- NSManagedObject sendability, custom executors, isolation conflictsperformance.md- Profiling with Instruments, reducing suspension points, execution strategiestesting.md- XCTest async patterns, Swift Testing, concurrency testing utilitiesmigration.md- Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migration
Verification Checklist (When You Change Concurrency Code)
- Confirm build settings (default isolation, strict concurrency, upcoming features) before interpreting diagnostics.
- Build — Verify the project compiles without new warnings or errors.
- Test — Run tests, especially concurrency-sensitive ones (see
references/testing.md). - Performance — If performance-related, verify with Instruments (see
references/performance.md). - Lifetime — If lifetime-related, verify deinit/cancellation behavior (see
references/memory-management.md). - Check
Task.isCancelledin long-running operations. - Never use semaphores or locks in async contexts — use actors or
Mutexinstead.
Glossary
See references/glossary.md for quick definitions of core concurrency terms used across this skill.
Note: This skill is based on the comprehensive Swift Concurrency Course by Antoine van der Lee.
GitHub Owner
Owner: avdlee
GitHub Links
- Twitter: https://twitter.com/twannl
SKILL.md
name: swift-concurrency description: 'Diagnose data races, convert callback-based code to async/await, implement actor isolation patterns, resolve Sendable conformance issues, and guide Swift 6 migration. Use when developers mention: (1) Swift Concurrency, async/await, actors, or tasks, (2) "use Swift Concurrency" or "modern concurrency patterns", (3) migrating to Swift 6, (4) data races or thread safety issues, (5) refactoring closures to async/await, (6) @MainActor, Sendable, or actor isolation, (7) concurrent code architecture or performance optimization, (8) concurrency-related linter warnings (SwiftLint or similar; e.g. async_without_await, Sendable/actor isolation/MainActor lint).'
Swift Concurrency
Agent Rules
- Analyze
Package.swiftor.pbxprojto determine Swift language mode (5.x vs 6) and toolchain before giving advice. - Before proposing fixes, identify the isolation boundary:
@MainActor, custom actor, actor instance isolation, or nonisolated. - Do not recommend
@MainActoras a blanket fix. Justify why main-actor isolation is correct for the code. - Prefer structured concurrency (child tasks, task groups) over unstructured tasks. Use
Task.detachedonly with a clear reason. - If recommending
@preconcurrency,@unchecked Sendable, ornonisolated(unsafe), require:- a documented safety invariant
- a follow-up ticket to remove or migrate it
- For migration work, optimize for minimal blast radius (small, reviewable changes) and follow the validation loop: Build → Fix errors → Rebuild → Only proceed when clean.
- Course references are for deeper learning only. Use them sparingly and only when they clearly help answer the developer's question.
Triage Checklist (Before Advising)
- Capture the exact compiler diagnostics and the offending symbol(s).
- Identify the current isolation boundary and module defaults (
@MainActor, custom actor, default isolation). - Confirm whether the code is UI-bound or intended to run off the main actor.
Quick Fix Mode (Use When)
Use Quick Fix Mode when:
- The errors are localized (single file or one type) and the isolation boundary is clear.
- The fix does not require API redesign or multi-module changes.
- You can explain the fix in 1–2 steps without changing behavior. Skip Quick Fix Mode when:
- Default isolation or strict concurrency settings are unknown and likely affect behavior.
- The error crosses module boundaries or involves public API changes.
- The fix would require
@unchecked Sendable,@preconcurrency, ornonisolated(unsafe)without a clear invariant.
Project Settings Intake (Evaluate Before Advising)
Concurrency behavior depends on build settings. Before advising, determine these via Read on Package.swift or Grep in .pbxproj files:
| Setting | SwiftPM (Package.swift) | Xcode (.pbxproj) |
|---|---|---|
| Default isolation | .defaultIsolation(MainActor.self) | SWIFT_DEFAULT_ACTOR_ISOLATION |
| Strict concurrency | .enableExperimentalFeature("StrictConcurrency=targeted") | SWIFT_STRICT_CONCURRENCY |
| Upcoming features | .enableUpcomingFeature("NonisolatedNonsendingByDefault") | SWIFT_UPCOMING_FEATURE_* |
| Language mode | // swift-tools-version: at top | Swift Language Version build setting |
| If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance. |
Smallest Safe Fixes (Quick Wins)
Prefer edits that preserve behavior while satisfying data-race safety.
- UI-bound types: isolate the type or specific members to
@MainActor(justify why UI-bound). - Global/static mutable state: move into an
actoror isolate to@MainActorif UI-only. - Background work: for work that should always hop off the caller’s isolation, move expensive work into an
asyncfunction marked@concurrent; for work that doesn’t touch isolated state but can inherit the caller’s isolation (for example withNonisolatedNonsendingByDefault), usenonisolatedwithout@concurrent, or use anactorto guard mutable state. - Sendable errors: prefer immutable/value types; avoid
@unchecked Sendableunless you can prove and document thread safety.
Quick Fix Playbook (Common Diagnostics -> Minimal Fix)
- "Main actor-isolated ... cannot be used from a nonisolated context"
- Quick fix: if UI-bound, make the caller
@MainActoror hop withawait MainActor.run { ... }. - Escalate if this is non-UI code or causes reentrancy; use
references/actors.md.
- Quick fix: if UI-bound, make the caller
- "Actor-isolated type does not conform to protocol"
- Quick fix: add isolated conformance (e.g.,
extension Foo: @MainActor SomeProtocol). - Escalate if the protocol requirements must be
nonisolated; usereferences/actors.md.
- Quick fix: add isolated conformance (e.g.,
- "Sending value of non-Sendable type ... risks causing data races"
- Quick fix: confine access inside an actor or convert to a value type with immutable (
let) state. - Escalate before
@unchecked Sendable; usereferences/sendable.mdandreferences/threading.md.
- Quick fix: confine access inside an actor or convert to a value type with immutable (
- SwiftLint
async_without_await- Quick fix: remove
asyncif not required; if required by protocol/override/@concurrent, use narrow suppression with rationale. Seereferences/linting.md.
- Quick fix: remove
- "wait(...) is unavailable from asynchronous contexts" (XCTest)
- Quick fix: use
await fulfillment(of:)or Swift Testing equivalents. Seereferences/testing.md.
- Quick fix: use
Escalation Path (When Quick Fixes Aren't Enough)
- Gather project settings (default isolation, strict concurrency level, upcoming features).
- Re-evaluate isolation boundaries and which types cross them.
- Use the decision tree + references for the deeper fix.
- If behavior changes are possible, document the invariant and add tests/verification steps.
Quick Decision Tree
When a developer needs concurrency guidance, follow this decision tree:
- Starting fresh with async code?
- Read
references/async-await-basics.mdfor foundational patterns - For parallel operations →
references/tasks.md(async let, task groups)
- Read
- Protecting shared mutable state?
- Need to protect class-based state →
references/actors.md(actors, @MainActor) - Need thread-safe value passing →
references/sendable.md(Sendable conformance)
- Need to protect class-based state →
- Managing async operations?
- Structured async work →
references/tasks.md(Task, child tasks, cancellation) - Streaming data →
references/async-sequences.md(AsyncSequence, AsyncStream)
- Structured async work →
- Working with legacy frameworks?
- Core Data integration →
references/core-data.md - General migration →
references/migration.md
- Core Data integration →
- Performance or debugging issues?
- Slow async code →
references/performance.md(profiling, suspension points) - Testing concerns →
references/testing.md(XCTest, Swift Testing)
- Slow async code →
- Understanding threading behavior?
- Read
references/threading.mdfor thread/task relationship and isolation
- Read
- Memory issues with tasks?
- Read
references/memory-management.mdfor retain cycle prevention
- Read
Triage-First Playbook (Common Errors -> Next Best Move)
- SwiftLint concurrency-related warnings
- Use
references/linting.mdfor rule intent and preferred fixes; avoid dummy awaits as “fixes”.
- Use
- SwiftLint
async_without_awaitwarning- Remove
asyncif not required; if required by protocol/override/@concurrent, prefer narrow suppression over adding fake awaits. Seereferences/linting.md.
- Remove
- "Sending value of non-Sendable type ... risks causing data races"
- First: identify where the value crosses an isolation boundary
- Then: use
references/sendable.mdandreferences/threading.md(especially Swift 6.2 behavior changes)
- "Main actor-isolated ... cannot be used from a nonisolated context"
- First: decide if it truly belongs on
@MainActor - Then: use
references/actors.md(global actors,nonisolated, isolated parameters) andreferences/threading.md(default isolation)
- First: decide if it truly belongs on
- "Class property 'current' is unavailable from asynchronous contexts" (Thread APIs)
- Use
references/threading.mdto avoid thread-centric debugging and rely on isolation + Instruments
- Use
- "Actor-isolated type does not conform to protocol" (protocol conformance errors)
- First: determine whether the protocol requirements must execute on the actor (for example, UI work on
@MainActor) or can safely benonisolated. - Then: follow the Quick Fix Playbook entry for actor-isolated protocol conformance and
references/actors.mdfor implementation patterns (isolated conformances,nonisolatedrequirements, and escalation steps).
- First: determine whether the protocol requirements must execute on the actor (for example, UI work on
- XCTest async errors like "wait(...) is unavailable from asynchronous contexts"
- Use
references/testing.md(await fulfillment(of:)and Swift Testing patterns)
- Use
- Core Data concurrency warnings/errors
- Use
references/core-data.md(DAO/NSManagedObjectID, default isolation conflicts)
- Use
Core Patterns Reference
Concurrency Tool Selection
| Need | Tool | Key Guidance |
|---|---|---|
| Single async operation | async/await | Default choice for sequential async work |
| Fixed parallel operations | async let | Known count at compile time; auto-cancelled on throw |
| Dynamic parallel operations | withTaskGroup | Unknown count; structured — cancels children on scope exit |
| Sync → async bridge | Task { } | Inherits actor context; use Task.detached only with documented reason |
| Shared mutable state | actor | Prefer over locks/queues; keep isolated sections small |
| UI-bound state | @MainActor | Only for truly UI-related code; justify isolation |
Common Scenarios
Network request with UI update
Task { @concurrent in
let data = try await fetchData()
await MainActor.run { self.updateUI(with: data) }
}
Processing array items in parallel
await withTaskGroup(of: ProcessedItem.self) { group in
for item in items {
group.addTask { await process(item) }
}
for await result in group {
results.append(result)
}
}
Swift 6 Migration Quick Guide
Key changes in Swift 6:
- Strict concurrency checking enabled by default
- Complete data-race safety at compile time
- Sendable requirements enforced on boundaries
- Isolation checking for all async boundaries
Migration Validation Loop
Apply this cycle for each migration change:
- Build — Run
swift buildor Xcode build to surface new diagnostics - Fix — Address one category of error at a time (e.g., all Sendable issues first)
- Rebuild — Confirm the fix compiles cleanly before moving on
- Test — Run the test suite to catch regressions (
swift testor Cmd+U) - Only proceed to the next file/module when all diagnostics are resolved
If a fix introduces new warnings, resolve them before continuing. Never batch multiple unrelated fixes — keep commits small and reviewable.
For detailed migration steps, see
references/migration.md.
Reference Files
Load these files as needed for specific topics:
async-await-basics.md- async/await syntax, execution order, async let, URLSession patternstasks.md- Task lifecycle, cancellation, priorities, task groups, structured vs unstructuredthreading.md- Thread/task relationship, suspension points, isolation domains, nonisolatedmemory-management.md- Retain cycles in tasks, memory safety patternsactors.md- Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutexsendable.md- Sendable conformance, value/reference types, @unchecked, region isolationlinting.md- Concurrency-focused lint rules and SwiftLintasync_without_awaitasync-sequences.md- AsyncSequence, AsyncStream, when to use vs regular async methodscore-data.md- NSManagedObject sendability, custom executors, isolation conflictsperformance.md- Profiling with Instruments, reducing suspension points, execution strategiestesting.md- XCTest async patterns, Swift Testing, concurrency testing utilitiesmigration.md- Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migration
Verification Checklist (When You Change Concurrency Code)
- Confirm build settings (default isolation, strict concurrency, upcoming features) before interpreting diagnostics.
- Build — Verify the project compiles without new warnings or errors.
- Test — Run tests, especially concurrency-sensitive ones (see
references/testing.md). - Performance — If performance-related, verify with Instruments (see
references/performance.md). - Lifetime — If lifetime-related, verify deinit/cancellation behavior (see
references/memory-management.md). - Check
Task.isCancelledin long-running operations. - Never use semaphores or locks in async contexts — use actors or
Mutexinstead.
Glossary
See references/glossary.md for quick definitions of core concurrency terms used across this skill.
Note: This skill is based on the comprehensive Swift Concurrency Course by Antoine van der Lee.