Use when adding new error messages to React, or seeing "unknown error code" warnings.
npx skills add jamesrochabrun/skills --skill "swift-concurrency"
Install specific skill from multi-skill repository
# Description
Guide for building, auditing, and refactoring Swift code using modern concurrency patterns (Swift 6+). This skill should be used when working with async/await, Tasks, actors, MainActor, Sendable types, isolation domains, or when migrating legacy callback/Combine code to structured concurrency. Covers Approachable Concurrency settings, isolated parameters, and common pitfalls.
# SKILL.md
name: swift-concurrency
description: Guide for building, auditing, and refactoring Swift code using modern concurrency patterns (Swift 6+). This skill should be used when working with async/await, Tasks, actors, MainActor, Sendable types, isolation domains, or when migrating legacy callback/Combine code to structured concurrency. Covers Approachable Concurrency settings, isolated parameters, and common pitfalls.
Swift Concurrency
Overview
This skill provides guidance for writing thread-safe Swift code using modern concurrency patterns. It covers three main workflows: building new async code, auditing existing code for issues, and refactoring legacy patterns to Swift 6+.
Core principle: Isolation is inherited by default. With Approachable Concurrency, code starts on MainActor and propagates through the program automatically. Opt out explicitly when needed.
Workflow Decision Tree
What are you doing?
β
βββΊ BUILDING new async code
β βββΊ See "Building Workflow" below
β
βββΊ AUDITING existing code
β βββΊ See "Auditing Checklist" below
β
βββΊ REFACTORING legacy code
βββΊ See "Refactoring Workflow" below
Building Workflow
When writing new async code, follow this decision process:
Step 1: Determine Isolation Needs
Does this type manage UI state or interact with UI?
β
βββΊ YES β Mark with @MainActor
β
βββΊ NO β Does it have mutable state shared across contexts?
β
βββΊ YES β Consider: Can it live on MainActor anyway?
β β
β βββΊ YES β Use @MainActor (simpler)
β β
β βββΊ NO β Use a custom actor (requires justification)
β
βββΊ NO β Leave non-isolated (default with Approachable Concurrency)
Step 2: Design Async Functions
// PREFER: Inherit caller's isolation (works everywhere)
func fetchData(isolation: isolated (any Actor)? = #isolation) async throws -> Data {
// Runs on whatever actor the caller is on
}
// USE WHEN: CPU-intensive work that must run in background
@concurrent
func processLargeFile() async -> Result { }
// AVOID: Non-isolated async without explicit choice
func ambiguousAsync() async { } // Where does this run?
Step 3: Handle Parallel Work
// For known number of independent operations
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
let (a, b) = await (avatar, banner)
// For dynamic number of operations
try await withThrowingTaskGroup(of: Void.self) { group in
for id in userIDs {
group.addTask { try await fetchUser(id) }
}
try await group.waitForAll()
}
Step 4: SwiftUI Integration
struct ProfileView: View {
@State private var avatar: Image?
var body: some View {
avatar
.task { avatar = await downloadAvatar() } // Auto-cancels on disappear
.task(id: userID) { /* Reloads when userID changes */ }
}
}
// For user actions
Button("Save") {
Task { await saveProfile() } // Inherits MainActor isolation
}
Auditing Checklist
When reviewing Swift concurrency code, check for these issues:
Critical Issues (Must Fix)
- [ ] Blocking the cooperative pool: Look for
DispatchSemaphore.wait(),DispatchGroup.wait(), or similar blocking calls inside async contexts - [ ] Data races: Non-Sendable types crossing isolation boundaries without proper handling
- [ ] Non-isolated async in non-Sendable types: These only work from non-isolated contexts
Common Issues (Should Fix)
- [ ] Actor overuse: Custom actors without justification (see "Actor Justification Test" in references)
- [ ] Unnecessary
MainActor.run: Should usually be@MainActoron the function instead - [ ] Thinking async = background: Synchronous CPU work inside async functions still blocks
- [ ] Unstructured Tasks where structured works:
Task { }instead ofasync letorTaskGroup - [ ] Missing cancellation handling: Long operations should check
Task.isCancelled
SwiftUI-Specific
- [ ] Views not MainActor-isolated: SwiftUI views should be
@MainActor(or use@Observable) - [ ] Accessing @State from detached tasks: Must hop back to MainActor
Sendable Compliance
- [ ] @unchecked Sendable overuse: Should be rare and justified
- [ ] Making everything Sendable: Not all types need to cross boundaries
- [ ] Non-Sendable closures escaping: Check closure captures
Refactoring Workflow
From Callbacks to async/await
// BEFORE: Callback-based
func fetchUser(id: Int, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, _, error in
if let error { completion(.failure(error)); return }
// ...
}.resume()
}
// AFTER: async/await with continuation
func fetchUser(id: Int) async throws -> User {
try await withCheckedThrowingContinuation { continuation in
fetchUser(id: id) { result in
continuation.resume(with: result)
}
}
}
From DispatchQueue to Actors
// BEFORE: Queue-based protection
class BankAccount {
private let queue = DispatchQueue(label: "account")
private var _balance: Double = 0
var balance: Double {
queue.sync { _balance }
}
func deposit(_ amount: Double) {
queue.async { self._balance += amount }
}
}
// AFTER: Actor (if truly needs own isolation)
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
}
// BETTER: MainActor class (if doesn't need concurrent access)
@MainActor
class BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
}
From Combine to AsyncSequence
// BEFORE: Combine publisher
cancellable = NotificationCenter.default
.publisher(for: .userDidLogin)
.sink { notification in /* ... */ }
// AFTER: AsyncSequence
for await _ in NotificationCenter.default.notifications(named: .userDidLogin) {
// Handle notification
}
Quick Reference
| Keyword | Purpose |
|---|---|
async |
Function can suspend |
await |
Suspension point |
Task { } |
Start async work, inherits isolation |
Task.detached { } |
Start async work, no inheritance |
@MainActor |
Runs on main thread |
actor |
Type with isolated mutable state |
nonisolated |
Opts out of actor isolation |
nonisolated(nonsending) |
Inherits caller's isolation |
@concurrent |
Always run on background (Swift 6.2+) |
Sendable |
Safe to cross isolation boundaries |
sending |
One-way transfer of non-Sendable |
async let |
Start parallel work |
TaskGroup |
Dynamic parallel work |
Approachable Concurrency Settings (Swift 6.2+)
For new Xcode 26+ projects, these are enabled by default:
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor
SWIFT_APPROACHABLE_CONCURRENCY = YES
Effects:
- Everything runs on MainActor unless explicitly marked otherwise
- nonisolated async functions stay on caller's actor instead of hopping to background
- Sendable errors become much rarer
Resources
For detailed technical reference, consult:
references/fundamentals.md- async/await, Tasks, structured concurrencyreferences/isolation.md- Actors, MainActor, isolation domains, inheritancereferences/sendable.md- Sendable protocol, non-Sendable patterns, isolated parametersreferences/common-mistakes.md- Detailed examples of what to avoidreferences/glossary.md- Complete terminology reference
Search patterns for references:
- Isolation: grep -i "isolation\|actor\|mainactor\|nonisolated"
- Sendable: grep -i "sendable\|sending\|boundary"
- Tasks: grep -i "task\|taskgroup\|async let\|structured"
# Supported AI Coding Agents
This skill is compatible with the SKILL.md standard and works with all major AI coding agents:
Learn more about the SKILL.md standard and how to use these skills with your preferred AI coding agent.