| 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
func fetchData(isolation: isolated (any Actor)? = #isolation) async throws -> Data {
}
@concurrent
func processLargeFile() async -> Result { }
func ambiguousAsync() async { }
Step 3: Handle Parallel Work
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
let (a, b) = await (avatar, banner)
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() }
.task(id: userID) { }
}
}
Button("Save") {
Task { await saveProfile() }
}
Auditing Checklist
When reviewing Swift concurrency code, check for these issues:
Critical Issues (Must Fix)
Common Issues (Should Fix)
SwiftUI-Specific
Sendable Compliance
Refactoring Workflow
From Callbacks to async/await
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()
}
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
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 }
}
}
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
}
@MainActor
class BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) {
balance += amount
}
}
From Combine to AsyncSequence
cancellable = NotificationCenter.default
.publisher(for: .userDidLogin)
.sink { notification in }
for await _ in NotificationCenter.default.notifications(named: .userDidLogin) {
}
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 concurrency
references/isolation.md - Actors, MainActor, isolation domains, inheritance
references/sendable.md - Sendable protocol, non-Sendable patterns, isolated parameters
references/common-mistakes.md - Detailed examples of what to avoid
references/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"