
Swift Concurrency
- 2.9k installs
- 944 repo stars
- Updated July 15, 2026
- dpearson2699/swift-ios-skills
A Swift 6 concurrency skill for fixing Sendable and actor isolation errors, adopting approachable concurrency, and writing data-race-safe async code.
About
Swift Concurrency helps developers review, fix, and write concurrent Swift code targeting Swift 6.3+ with actor isolation, Sendable safety, and structured concurrency patterns. The triage workflow captures compiler diagnostics, concurrency settings including Approachable Concurrency and Default MainActor isolation, and whether code is UI-bound before applying the smallest safe fix. Swift 6.2 changes cover SE-0466 default MainActor isolation, SE-0461 nonisolated(nonsending), @concurrent for background work, SE-0472 Task.immediate, and SE-0475 Observations for @Observable types. Actor rules require protecting mutable shared state, using @MainActor for UI code, and avoiding manual locks inside actors. Sendable rules favor immutable value types, document @unchecked Sendable as last resort, and use sending parameters for cross-isolation callbacks. Structured concurrency patterns include async let, TaskGroup, task cancellation with .task in SwiftUI, and actor reentrancy awareness across await points. Common mistakes span blocking MainActor, unnecessary actors, Task.detached overuse, semaphores in async code, and GCD APIs. A review checklist confirms isolation, cancellation, and @concurr.
- Triage workflow: capture diagnostics, settings, then smallest safe fix
- Swift 6.2: default MainActor isolation, @concurrent, nonisolated(nonsending)
- Actor rules: protect mutable state, @MainActor for UI, no locks inside actors
- Structured concurrency with TaskGroup, async let, and cooperative cancellation
- Review checklist for Sendable, reentrancy, and blocking on MainActor
Swift Concurrency by the numbers
- 2,881 all-time installs (skills.sh)
- +125 installs in the week ending Jul 29, 2026 (Skillselion tracking)
- Ranked #54 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 31, 2026 (Skillselion catalog sync)
swift-concurrency capabilities & compatibility
- Capabilities
- triage compiler diagnostics with concurrency set · apply @mainactor, @concurrent, and isolated conf · use taskgroup, async let, and task.immediate pat · handle actor reentrancy and cooperative task can · bridge callbacks with asyncstream and checked co · audit against review checklist for locks, sendab
- Platforms
- macOS
- Runs
- Runs locally
- Pricing
- Free
What swift-concurrency says it does
All mutable shared state MUST be protected by an actor or global actor.
Never use DispatchQueue, DispatchGroup, DispatchSemaphore, or any GCD API. Use async/await, actors, and TaskGroups instead.
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-concurrencyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2.9k |
|---|---|
| repo stars | ★ 944 |
| Security audit | 3 / 3 scanners passed |
| Last updated | July 15, 2026 |
| Repository | dpearson2699/swift-ios-skills ↗ |
How do you resolve Swift 6 strict concurrency diagnostics without unsafe @unchecked Sendable shortcuts or MainActor blocking?
Fix Swift 6 concurrency errors, adopt approachable concurrency (SE-0466), and write data-race-safe async code with actors and Sendable rules.
Who is it for?
iOS developers hitting Sendable conformance errors, actor isolation warnings, or migrating to Swift 6 strict concurrency mode.
Skip if: Skip when the project is not Swift-based or concurrency settings are not yet on Swift 6 language mode.
When should I use this skill?
Use when fixing Sendable errors, actor isolation warnings, strict concurrency diagnostics, or adopting SE-0466 approachable concurrency.
What you get
Data-race-safe code with correct actor isolation, @concurrent background work, structured concurrency, and resolved compiler diagnostics.
- Concurrency build-setting recommendations
- Actor isolation code fixes
By the numbers
- Eval fixtures reference Swift 6.3 and Xcode 26 concurrency scenarios
Files
Swift Concurrency
Review, fix, and write concurrent Swift code targeting Swift 6.3+. 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+).
- 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.
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: 1. Ensure the containing type is nonisolated (or the function itself is). 2. Add @concurrent to the function. 3. Add async if not already asynchronous. 4. Add await at 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 bootActor Isolation Rules
1. All mutable shared state MUST be protected by an actor or global actor. 2. @MainActor for all UI-touching code. No exceptions. 3. Use nonisolated only for methods that access immutable (let) properties or are pure computations. 4. Use @concurrent to explicitly move work off the caller's actor. 5. Never use nonisolated(unsafe) unless you have proven internal synchronization and exhausted all other options. 6. Never add manual locks (NSLock, DispatchSemaphore) inside actors.
Sendable Rules
1. Value types (structs, enums) are automatically Sendable when all stored properties are Sendable. 2. Actors are implicitly Sendable. 3. @MainActor classes are implicitly Sendable. Do NOT add redundant Sendable conformance. 4. Non-actor classes: must be final with all stored properties let and Sendable. 5. @unchecked Sendable is a last resort. Document why the compiler cannot prove safety. 6. Use sending parameters (SE-0430) for finer-grained isolation control. 7. Use @preconcurrency import only for third-party libraries you cannot modify. Plan to remove it.
Structured Concurrency Patterns
Async Defer
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 Cancellation
- Cancellation is cooperative. Check
Task.isCancelledor call
try Task.checkCancellation() in loops.
- Use
.taskmodifier in SwiftUI -- it handles cancellation on view disappear. - Use
withTaskCancellationHandlerfor cleanup. - 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@Observable
properties as an AsyncSequence.
Synchronization Primitives
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+,
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 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.
Common Mistakes
1. Blocking the main actor. Heavy computation on @MainActor freezes UI. Move to a @concurrent function. 2. Unnecessary @MainActor. Network layers, data processing, and model code do not need @MainActor. Only UI-touching code does. 3. Actors for stateless code. No mutable state means no actor needed. Use a plain struct or function. 4. Actors for immutable data. Use a Sendable struct, not an actor. 5. Task.detached without good reason. Loses priority, task-local values, and cancellation propagation. 6. Forgetting task cancellation. Store Task references and cancel them, or use the .task view modifier. 7. Retain cycles in Tasks. Use [weak self] when capturing self in long-lived stored tasks. 8. Semaphores in async context. DispatchSemaphore.wait() in async code will deadlock. Use structured concurrency instead. 9. Split isolation. Mixing @MainActor and nonisolated properties in one type. Isolate the entire type consistently. 10. MainActor.run instead of static isolation. Prefer @MainActor func over await MainActor.run { }. 11. 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
{
"skill_name": "swift-concurrency",
"evals": [
{
"id": 0,
"prompt": "Review this Swift 6.3/Xcode 26 concurrency build-settings plan: the team says Approachable Concurrency alone makes the whole module MainActor, strict concurrency should start at Targeted in Swift 6 mode, and CPU-heavy image decoding can stay on MainActor because nonisolated async functions now hop to a background executor. Correct the plan and give the smallest build-setting and code-level recommendations.",
"expected_output": "A source-grounded settings review that separates Approachable Concurrency from Default Actor Isolation, states that Swift 6 mode uses complete strict concurrency with errors, and recommends @concurrent/nonisolated offloading for CPU-heavy work instead of blocking MainActor.",
"files": [],
"assertions": [
"Separates Xcode's Approachable Concurrency setting from Default Actor Isolation and says MainActor-by-default requires Default Actor Isolation = MainActor.",
"States that Swift 6 / 6.3 language mode uses complete strict concurrency and emits errors, while Targeted or Minimal are Swift 5 migration settings.",
"Explains that nonisolated async functions stay on the caller's actor with nonisolated(nonsending) behavior unless @concurrent is used.",
"Recommends offloading CPU-heavy work with @concurrent and a nonisolated context rather than leaving it on MainActor.",
"Avoids broad architecture or SwiftUI state-management redesign beyond the concurrency settings and code-level fix."
]
},
{
"id": 1,
"prompt": "A SwiftUI app with Default Actor Isolation set to MainActor has errors around a @Observable StickerModel conforming to Exportable, a PhotoProcessor async method, and a legacy SDK imported with @preconcurrency. Give a diagnostic remediation plan that preserves behavior, avoids unchecked Sendable unless truly justified, and explains which fixes belong in swift-concurrency rather than architecture or navigation skills.",
"expected_output": "A diagnostic remediation plan that uses @MainActor or isolated conformances for UI-bound state, explains nonisolated(nonsending) and @concurrent tradeoffs for PhotoProcessor work, treats @preconcurrency as temporary, and keeps architecture/navigation details as sibling handoffs.",
"files": [],
"assertions": [
"Uses @MainActor isolation or an isolated conformance such as extension StickerModel: @MainActor Exportable for UI-bound protocol conformance errors.",
"Explains when PhotoProcessor can stay on the caller's actor and when CPU-heavy work should move to @concurrent/nonisolated async code.",
"Prefers value immutability, actor isolation, or synchronization over adding @unchecked Sendable by default.",
"Treats @preconcurrency import as a temporary bridge for third-party modules with a documented removal plan.",
"Routes module architecture, navigation implementation, and detailed SwiftUI state ownership to sibling skills instead of expanding this skill's scope."
]
},
{
"id": 2,
"prompt": "Review this synchronization plan for a Swift 6.3 app: use an actor for a synchronous metrics counter called from C callbacks, put NSLock inside an actor for cache mutation, hold a Mutex while awaiting a network fetch, and ban NSLock because it is not Sendable. Replace it with a modern concurrency-safe plan for iOS 16 through iOS 26.",
"expected_output": "A synchronization review that chooses actors for async shared state, Mutex or OSAllocatedUnfairLock for synchronous state depending on deployment target, Atomic for simple counters or flags, avoids locks inside actors or across await, and corrects the NSLock Sendable claim without recommending it first.",
"files": [],
"assertions": [
"Uses an actor for shared mutable state when asynchronous access and actor hops are acceptable.",
"Uses Mutex for new code targeting iOS 18+ and OSAllocatedUnfairLock for iOS 16-17 support or ownership-assertion needs.",
"Uses Atomic for simple scalar counters or flags rather than an actor or full lock when appropriate.",
"Rejects locks inside actors and rejects holding any lock across await suspension points.",
"Corrects the claim that NSLock is not Sendable while still preferring modern state-protecting primitives for new Swift concurrency code."
]
}
]
}
Approachable Concurrency Quick Reference
Use this reference when the project has opted into the Swift 6.2 approachable concurrency settings and, when appropriate, default MainActor isolation.
Detecting the Mode
Xcode 26: Check build settings under Swift Compiler > Concurrency:
- Swift language version: 6.2+
- Approachable Concurrency: enabled when using the bundled upcoming-feature flags
(NonisolatedNonsendingByDefault, isolated-conformance inference, inferred Sendable captures, and related usability flags).
- Default Actor Isolation:
MainActorwhen unannotated code should infer
@MainActor isolation.
- Strict Concurrency Checking: Swift 6 language mode is complete and emits
errors; Complete / Targeted / Minimal are migration settings for earlier language modes.
SwiftPM: Inspect Package.swift swiftSettings for the corresponding flags.
Behavior Changes
Async functions stay on the caller's actor
In Swift 6.2, nonisolated async functions no longer hop to the global concurrent executor. They stay on whichever actor called them. This eliminates many "sending X risks causing data races" errors.
Default MainActor isolation
With Default Actor Isolation set to MainActor, unannotated declarations in the module are inferred as @MainActor. This means:
- Global and static variables are protected by default.
- Protocol conformances are implicitly isolated.
- Mutable state is safe without explicit annotation.
Isolated conformances
Protocol conformances can be explicitly isolated: extension Foo: @MainActor SomeProtocol. The compiler prevents using the conformance outside the matching isolation context.
Applying Fixes in This Mode
- Prefer minimal annotations. Let default MainActor isolation do the work
for UI-bound code.
- Use isolated conformances instead of
nonisolatedworkarounds for
protocol conformances.
- Keep global/shared mutable state on MainActor unless there is a clear
performance need to offload.
- Remove redundant `@MainActor` annotations that are now implied by the
default isolation mode.
Offloading Work
- Use
@concurrenton async functions that must run on the concurrent pool. - Make types or members
nonisolatedonly when they are truly thread-safe and
used off the main actor.
- Continue to respect
Sendableboundaries when values cross actors or tasks.
Common Pitfalls
| Pitfall | Why it happens | Fix |
|---|---|---|
| CPU-heavy work on MainActor | Default isolation hides the problem | Move to @concurrent async function |
Task.detached breaking isolation | Ignores inherited actor context | Use Task { } unless you truly need detachment |
Redundant @MainActor everywhere | Default isolation already provides it | Remove explicit annotations |
nonisolated on mutable state | Breaks the safety guarantee | Keep mutable state isolated |
Concurrency Keywords
| Keyword | What it does |
|---|---|
async | Function can suspend |
await | Suspend here until done |
Task { } | Start async work, inherits context |
Task.detached { } | Start async work, no inherited context |
Task.immediate { } | Start immediately on current actor |
@MainActor | Runs on main thread |
actor | Type with isolated mutable state |
nonisolated | Opts out of actor isolation |
Sendable | Safe to pass between isolation domains |
@concurrent | Always run on background thread pool (Swift 6.2+) |
async let | Start parallel work (fixed count) |
TaskGroup | Dynamic parallel work |
sending | Parameter-level isolation transfer (SE-0430) |
Swift Async Algorithms
swift-async-algorithms is an Apple open-source package providing AsyncSequence algorithms modeled after the standard library's Sequence algorithms.
Contents
Add to Package.swift:
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0")Key Algorithms
Combining
import AsyncAlgorithms
// Merge multiple sequences into one (interleaved by arrival time)
for await value in merge(streamA, streamB) {
handle(value)
}
// Combine latest values from two sequences (emits when either updates)
for await (a, b) in combineLatest(streamA, streamB) {
handle(a, b)
}
// Zip — pairs elements 1:1 (waits for both)
for await (a, b) in zip(streamA, streamB) {
handle(a, b)
}
// Chain — concatenate sequences end-to-end
for await value in chain(streamA, streamB) {
handle(value)
}Temporal
// Debounce — emit after a quiet period (e.g., search-as-you-type)
let searchResults = searchTerms
.debounce(for: .milliseconds(300))
for await term in searchResults {
await performSearch(term)
}
// Throttle — emit at most once per interval
let throttled = sensorReadings
.throttle(for: .seconds(1))
for await reading in throttled {
updateDisplay(reading)
}
// Chunks — collect elements into arrays by count or time
for await batch in events.chunks(ofCount: 10) {
await processBatch(batch) // [Event] with up to 10 elements
}
for await batch in events.chunked(by: .repeating(every: .seconds(1))) {
await processBatch(batch)
}Filtering and Transformation
// Remove consecutive duplicates
for await value in stream.removeDuplicates() {
handle(value)
}
// Compacted — remove nils (like compactMap without transform)
let values: AsyncStream<Int?> = ...
for await value in values.compacted() {
// value is non-optional Int
}Common Patterns
Search-as-you-type
func searchResults(for terms: AsyncStream<String>) -> AsyncStream<[Result]> {
AsyncStream { continuation in
Task {
for await term in terms.debounce(for: .milliseconds(300)) {
guard !Task.isCancelled else { break }
let results = try? await searchService.search(term)
continuation.yield(results ?? [])
}
continuation.finish()
}
}
}Rate-limited API calls
for await batch in requestStream.chunks(ofCount: 50).throttle(for: .seconds(1)) {
await api.sendBatch(batch)
}Bridging and Interop
Patterns for bridging callback-based, delegate-based, and GCD code into Swift Concurrency.
Contents
Checked Continuations
Use withCheckedContinuation (non-throwing) or withCheckedThrowingContinuation (throwing) to bridge completion-handler APIs into async/await. Available iOS 13+.
Docs: withCheckedContinuation) · withCheckedThrowingContinuation)
Basic Pattern
func fetchData() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyFetch { result in
switch result {
case .success(let data):
continuation.resume(returning: data)
case .failure(let error):
continuation.resume(throwing: error)
}
}
}
}Rules
- Resume exactly once. Missing resume suspends the task forever (leak). Double resume crashes at runtime.
- Prefer checked over unsafe.
withCheckedContinuationdetects misuse at runtime with diagnostics. UsewithUnsafeContinuationonly in performance-critical paths after correctness is proven. - Capture continuation carefully. The continuation escapes the closure — ensure all code paths resume it, including error and cancellation paths.
Delegate Bridging
class LocationBridge: NSObject, CLLocationManagerDelegate {
private var continuation: CheckedContinuation<CLLocation, any Error>?
private let manager = CLLocationManager()
func requestLocation() async throws -> CLLocation {
try await withCheckedThrowingContinuation { continuation in
self.continuation = continuation
manager.delegate = self
manager.requestLocation()
}
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
continuation?.resume(returning: locations[0])
continuation = nil
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?.resume(throwing: error)
continuation = nil
}
}Cancellation Support
func fetchWithCancellation() async throws -> Data {
try await withTaskCancellationHandler {
try await withCheckedThrowingContinuation { continuation in
let task = legacyFetch { result in
switch result {
case .success(let data): continuation.resume(returning: data)
case .failure(let error): continuation.resume(throwing: error)
}
}
// Store task for cancellation
}
} onCancel: {
// Cancel the underlying work
}
}AsyncStream from Callbacks
For APIs that deliver multiple values over time (delegates, NotificationCenter), use AsyncStream:
func locationUpdates() -> AsyncStream<CLLocation> {
AsyncStream { continuation in
let delegate = StreamingLocationDelegate(continuation: continuation)
continuation.onTermination = { _ in
delegate.stop()
}
delegate.start()
}
}GCD Migration
| GCD Pattern | Swift Concurrency Replacement |
|---|---|
DispatchQueue.main.async { } | @MainActor isolation or MainActor.run { } |
DispatchQueue.global().async { } | Task { } or Task.detached { } (Swift 6.2: @concurrent) |
DispatchGroup | async let or TaskGroup |
DispatchSemaphore | Actor isolation or AsyncStream |
DispatchWorkItem with cancel | Task with task.cancel() |
DispatchQueue serial queue | actor |
DispatchQueue.concurrentPerform | withTaskGroup |
DispatchSource.makeTimerSource | Task.sleep(for:) in a loop, or Clock |
DispatchGroup → TaskGroup
// Before (GCD)
let group = DispatchGroup()
for url in urls {
group.enter()
fetch(url) { _ in group.leave() }
}
group.notify(queue: .main) { updateUI() }
// After (Swift Concurrency)
let results = await withTaskGroup(of: Data?.self) { group in
for url in urls {
group.addTask { try? await fetch(url) }
}
return await group.reduce(into: [Data]()) { if let d = $1 { $0.append(d) } }
}
updateUI(results)Serial Queue → Actor
// Before
let serialQueue = DispatchQueue(label: "com.app.cache")
serialQueue.async { self.cache[key] = value }
// After
actor Cache {
private var storage: [String: Data] = [:]
func set(_ key: String, _ value: Data) { storage[key] = value }
func get(_ key: String) -> Data? { storage[key] }
}Concurrency Patterns
Approachable concurrency patterns introduced in Swift 6.2+ — a philosophy shift where code stays single-threaded by default until you choose to introduce concurrency.
Contents
- Core Problem Solved
- SE-0466: Default MainActor Isolation
- SE-0461: nonisolated(nonsending)
- `@concurrent Attribute`
- SE-0472: Task.immediate
- Isolated Conformances
- SE-0481: weak let
- SE-0475: Transactional Observation (Observations)
- Global and Static State
- Migration and Build Settings
- Summary
Core Problem Solved
In Swift 6.0/6.1, data-race safety was enforced at compile time, but the most natural code to write often produced data-race errors. Async functions on types with mutable state would implicitly hop to the global concurrent executor, causing send-safety violations even when no actual parallelism was intended.
// Swift 6.0/6.1: This produces a data-race error
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { /* ... */ }
}
@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 }
// Error: Sending 'self.photoProcessor' risks causing data races
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}// Swift 6.2: The same code compiles without error
// because extractSticker stays on the caller's actor
class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? { /* ... */ }
}
@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 }
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}SE-0466: Default MainActor Isolation
Enable with the -default-isolation MainActor compiler flag, SwiftPM .defaultIsolation(MainActor.self), or Xcode's separate Default Actor Isolation build setting set to MainActor.
Do not confuse this with Xcode's Approachable Concurrency build setting, which enables a bundle of upcoming-feature flags such as nonisolated-nonsending by default, isolated-conformance inference, inferred Sendable captures, and related global-actor usability changes.
What it does:
- Unannotated declarations in the module are inferred as
@MainActorunless
opted out.
- Global and static variables are protected by the main actor by default.
- Protocol conformances are implicitly isolated to
@MainActor. - Eliminates most annotation burden for single-threaded UI code.
Recommended for: Apps, scripts, and executable targets. Not recommended for library targets that should remain actor-agnostic.
// With default MainActor isolation -- no @MainActor annotations needed:
final class StickerLibrary {
static let shared = StickerLibrary()
}
final class StickerModel {
let photoProcessor = PhotoProcessor()
var selection: [PhotosPickerItem] = []
}
extension StickerModel: Exportable {
func export() { photoProcessor.exportAsPNG() }
}SE-0461: nonisolated(nonsending)
Nonisolated async functions stay on the caller's actor by default instead of hopping to the global concurrent executor. This is the nonisolated(nonsending) default behavior.
Key implication: Values passed into an async function are never sent outside the actor, eliminating data races without annotation.
To explicitly opt into background execution, use @concurrent.
@concurrent Attribute
Ensures a function always runs on the concurrent thread pool, freeing the calling actor for other work.
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 { /* ... */ }
}Steps to offload a function to background: 1. Ensure the containing type is nonisolated (or the function itself). 2. Add @concurrent to the function. 3. Add async if not already asynchronous. 4. Add await at call sites.
nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { /* ... */ }
}
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. There is also Task.immediateDetached which combines immediate start with detached semantics.
Task.immediate { await handleUserInput() }Use for latency-sensitive work where enqueue delay is unacceptable.
Isolated Conformances
A conformance that needs MainActor state is called an isolated conformance. The compiler ensures the conformance is only used in a matching isolation context.
protocol Exportable {
func export()
}
extension StickerModel: @MainActor Exportable {
func export() { photoProcessor.exportAsPNG() }
}
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // OK -- on MainActor
}
}
// But in a nonisolated context:
nonisolated struct GenericExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
// Error: Main actor-isolated conformance of 'StickerModel' to
// 'Exportable' cannot be used in nonisolated context
items.append(item)
}
}SE-0481: weak let
Immutable weak references (weak let) enable Sendable conformance for types that hold weak references, since immutability guarantees thread safety. SE-0481 is implemented in Swift 6.3.
SE-0475: Transactional Observation (Observations)
Observations { } provides transactional observation of @Observable types via AsyncSequence.
for await _ in Observations { model.count } {
print("Count changed to \(model.count)")
}Global and Static State
Global and static variables are prone to data races. The most common protection is @MainActor:
@MainActor
final class StickerLibrary {
static let shared = StickerLibrary() // protected by MainActor
}With default MainActor isolation (SE-0466), this annotation is implicit.
Migration and Build Settings
All approachable concurrency features are opt-in via:
- Xcode 26: Swift Compiler > Concurrency section in build settings.
- SwiftPM:
swiftSettingsin Package.swift using theSwiftSettingAPI.
For Swift 6 language mode, strict concurrency checking is complete and data-race diagnostics are errors. Use Targeted or Minimal only as Swift 5 migration settings while preparing code for Swift 6.
Swift 6.2 includes migration tooling to help make necessary code changes automatically. See swift.org/migration for details.
Summary
The Swift 6.2 concurrency progression: 1. Start with code that runs on the main actor by default (no data race risk). 2. Async functions run wherever they are called from (still no data race risk). 3. When you need performance, offload specific code with @concurrent.
Concurrency Diagnostics
Common Swift concurrency compiler warnings and errors with their fixes.
Diagnostic → Fix Reference
| Diagnostic | Cause | Fix |
|---|---|---|
Sending 'x' risks causing data races | Passing a non-Sendable value across isolation boundaries | Make the type Sendable, use sending parameter, or restructure to avoid the crossing |
Capture of 'x' with non-sendable type in a @Sendable closure | Closure captures non-Sendable value | Make type Sendable, copy the value, or use an actor |
Non-sendable type 'X' returned by implicitly asynchronous call | Returning non-Sendable from cross-isolation call | Make return type Sendable or keep work on same isolation |
Main actor-isolated property 'x' can not be mutated from a nonisolated context | Accessing @MainActor state from non-main context | Add @MainActor to caller, use await MainActor.run { }, or restructure |
Call to main actor-isolated function in a synchronous nonisolated context | Calling @MainActor function without await | Add await, annotate caller @MainActor, or use .task { } in SwiftUI |
Actor-isolated property 'x' can not be referenced from a nonisolated context | Accessing actor state without await | Add await or move code into the actor |
Type 'X' does not conform to protocol 'Sendable' | Stored properties aren't all Sendable | Prefer an immutable value type or immutable snapshot; otherwise use actor isolation or synchronization. Use @unchecked Sendable only with documented internal locking |
Passing closure as a 'sending' parameter risks causing data races | Closure captures values that could race | Ensure captures are Sendable or don't escape the isolation domain |
Task-isolated value of type 'X' passed as a strongly transferred parameter | Moving a value out of a task unsafely | Copy the value or use sending return |
Global variable 'x' is not concurrency-safe | Mutable global without isolation | Add @MainActor, make it nonisolated(unsafe) (with justification), or use actor |
Swift 6.2 Approachable Concurrency Changes
With UpcomingFeature.InferSendableFromCaptures (Swift 6.2):
- Closures infer Sendable from their captures (fewer explicit annotations needed)
nonisolated(nonsending)is the new default for async functions (SE-0461)@concurrentmarks functions that intentionally run off-actor
Strict Concurrency Adoption Strategy
1. Know the language mode. In Swift 6 / 6.3 language mode, strict concurrency checking is complete and data-race diagnostics are errors. 2. Use migration levels only before Swift 6. In Swift 5 language mode, Targeted or Minimal can stage adoption before moving to Complete. 3. Fix diagnostics bottom-up. Start with leaf types (models, DTOs), then services, then UI. 4. Use `@preconcurrency import` temporarily for third-party modules that haven't adopted Sendable — document removal plan.
For Sendable diagnostics, check this order before adding annotations: immutable struct / enum, immutable let properties on a final class, actor or global-actor isolation, a small synchronized wrapper, then @unchecked Sendable only when the compiler cannot see a proven lock invariant.
Runtime Diagnostics
Enable the Thread Sanitizer (-sanitize=thread) to catch data races at runtime that the compiler can't statically prove.
Xcode: Edit Scheme → Run → Diagnostics → Thread Sanitizer.
Common TSan findings in concurrent code:
- Simultaneous reads and writes to
DictionaryorArraywithout actor protection - Delegate callbacks arriving on unexpected queues
- Completion handlers racing with synchronous property access
SwiftUI Concurrency Guide
Concurrency patterns and best practices specific to SwiftUI applications.
Contents
- MainActor Default in SwiftUI
- Where SwiftUI Runs Code Off the Main Thread
- Sendable Closures and Data-Race Safety
- Structuring Async Work
- The .task Modifier
- `@Observable View Models`
- Async Observation with Observations (SE-0475)
- Performance-Driven Concurrency
- Common SwiftUI Concurrency Mistakes
MainActor Default in SwiftUI
Viewis@MainActorisolated by default;bodyand all members inherit
this isolation.
- Swift 6.2 can infer
@MainActorfor all types in a module via default actor
isolation (SE-0466).
- This default aligns with UIKit/AppKit
@MainActorAPIs and simplifies UI
code.
Where SwiftUI Runs Code Off the Main Thread
SwiftUI may evaluate some view logic on background threads for performance:
Shapepath generationLayoutmethods (sizeThatFits,placeSubviews)visualEffectclosuresonGeometryChangeclosures
These APIs often require Sendable closures to reflect their off-main-thread runtime semantics.
Sendable Closures and Data-Race Safety
Accessing @MainActor state from a Sendable closure is unsafe and flagged by the compiler.
Fix: Capture value copies in the closure capture list.
// WRONG: Captures @MainActor state directly
.visualEffect { content, proxy in
content.offset(y: self.offset) // Error: @MainActor state in Sendable closure
}
// CORRECT: Capture a copy
let currentOffset = offset
// ... use in closure:
.visualEffect { [currentOffset] content, proxy in
content.offset(y: currentOffset)
}Avoid sending self into a Sendable closure just to read a single property.
Structuring Async Work
SwiftUI action callbacks are synchronous so UI updates (like loading states) can be immediate.
struct ContentView: View {
@State private var isLoading = false
@State private var result: String?
var body: some View {
Button("Load") {
isLoading = true // Immediate UI update
Task {
result = await fetchData()
isLoading = false
}
}
}
}Pattern: Use state as the boundary. Async work updates model/state; UI reacts synchronously.
The .task Modifier
Prefer .task over manual Task creation in views:
.task {
await loadInitialData()
}Advantages:
- Automatically cancels on view disappear.
- Inherits the view's actor isolation (
@MainActor). - No need to store
Taskreferences for cancellation.
Use .task(id:) to restart work when a value changes:
.task(id: selectedItem) {
details = await fetchDetails(for: selectedItem)
}@Observable View Models
- Annotate view models with both
@Observableand@MainActor. - Use
@Stateto own an@Observableinstance (replaces@StateObject). - Avoid
@ObservedObject/@StateObject/ObservableObjectin new code.
@Observable @MainActor
final class ViewModel {
var items: [Item] = []
var isLoading = false
func load() async {
isLoading = true
items = await fetchItems()
isLoading = false
}
}
struct ItemListView: View {
@State private var viewModel = ViewModel()
var body: some View {
List(viewModel.items) { item in
Text(item.name)
}
.task { await viewModel.load() }
}
}Async Observation with Observations (SE-0475)
Use Observations { } for transactional async observation:
.task {
for await _ in Observations { viewModel.searchText } {
await viewModel.performSearch()
}
}Performance-Driven Concurrency
- Offload expensive work from the main actor to avoid hitches.
- Keep time-sensitive UI logic (animations, gesture responses) synchronous.
- Separate UI code from long-running async work.
@Observable @MainActor
final class ImageProcessor {
var processedImage: UIImage?
func process(data: Data) async {
// Offload heavy work
let result = await Self.runProcessing(data: data)
processedImage = result
}
@concurrent
nonisolated static func runProcessing(data: Data) async -> UIImage {
// Runs on background thread pool
// ...
}
}Common SwiftUI Concurrency Mistakes
1. Creating `Task` in `body`. Use .task modifier instead. 2. Not cancelling tasks. .task does this automatically; manual Task references must be cancelled in onDisappear. 3. Blocking MainActor in view updates. Move heavy computation to @concurrent functions. 4. Using `Task.detached` in views. Loses actor context. Use Task { } or .task modifier. 5. Updating state from background. Always update @State / @Observable properties on @MainActor.
Synchronization Primitives
Low-level synchronization tools for protecting shared mutable state when actors are not the right fit. All primitives discussed here are Sendable and safe to use from multiple threads.
Contents
Mutex
Module: Synchronization · Availability: iOS 18.0+
Mutex<Value> is a synchronization primitive that protects shared mutable state via mutual exclusion. It blocks threads attempting to acquire the lock, ensuring only one execution context accesses the protected value at a time.
Documentation: sosumi.ai/documentation/synchronization/mutex
Basic Usage
import Synchronization
class ImageCache: Sendable {
let storage = Mutex<[String: UIImage]>([:])
func image(forKey key: String) -> UIImage? {
storage.withLock { $0[key] }
}
func store(_ image: UIImage, forKey key: String) {
storage.withLock { $0[key] = image }
}
func removeAll() {
storage.withLock { $0.removeAll() }
}
}withLockIfAvailable
Use withLockIfAvailable to attempt acquisition without blocking. Returns nil if the lock is already held.
let counter = Mutex<Int>(0)
// Non-blocking attempt — returns nil if lock is contended
if let value = counter.withLockIfAvailable({ $0 }) {
print("Current count: \(value)")
} else {
print("Lock was busy, skipping")
}Key Properties
- Generic over `Value`: The protected state is stored inside the mutex,
making it clear what the lock protects.
- `Sendable`:
Mutexconforms toSendable, so it can be stored in
Sendable types (classes, actors, global state).
- Non-recursive: Attempting to lock a
Mutexthat you already hold on the
same thread is undefined behavior.
- Synchronous only: Do not
awaitinsidewithLock. The lock is held for
the duration of the closure — blocking across a suspension point will deadlock or starve other threads.
OSAllocatedUnfairLock
Module: os · Availability: iOS 16.0+
OSAllocatedUnfairLock<State> wraps os_unfair_lock in a safe Swift API. It heap-allocates the underlying lock, avoiding the unsound address-of problem that makes raw os_unfair_lock unusable from Swift.
Documentation: sosumi.ai/documentation/os/osallocatedunfairlock
State-Protecting Lock
import os
enum LoadState: Sendable {
case idle
case loading
case complete(Data)
case failed(Error)
}
final class ResourceLoader: Sendable {
let state = OSAllocatedUnfairLock(initialState: LoadState.idle)
func beginLoading() {
state.withLock { $0 = .loading }
}
func completeLoading(with data: Data) {
state.withLock { $0 = .complete(data) }
}
var currentState: LoadState {
state.withLock { $0 }
}
}Stateless Lock
When protecting external state or a code section rather than a specific value:
let lock = OSAllocatedUnfairLock()
lock.withLock {
// Critical section — no associated state
writeToSharedFile(data)
}Manual lock/unlock
Available but discouraged. Must unlock from the same thread that locked. Never use across await suspension points.
lock.lock()
defer { lock.unlock() }
// Critical sectionMutex vs OSAllocatedUnfairLock
Mutex<Value> | OSAllocatedUnfairLock<State> | |
|---|---|---|
| Availability | iOS 18+ | iOS 16+ |
| Module | Synchronization | os |
| State model | Value stored inside lock (generic Value) | Optional state via initialState: |
| `withLockIfAvailable` | Returns nil on contention | Returns nil on contention |
| Ownership assertions | Not available | precondition(.owner) / precondition(.notOwner) |
| Manual lock/unlock | Not available | Available (lock() / unlock()) |
| Recommendation | Preferred for iOS 18+ code | Use when targeting iOS 16–17 |
Guideline: Use Mutex for new code targeting iOS 18+. For apps that run on iOS 16 through current releases, either keep the shared abstraction backed by OSAllocatedUnfairLock or branch with #available(iOS 18, *) so iOS 18+ uses Mutex and iOS 16–17 uses OSAllocatedUnfairLock. Prefer OSAllocatedUnfairLock when you need ownership assertions for debugging.
Atomic
Module: Synchronization · Availability: iOS 18.0+
Atomic<Value> provides lock-free atomic operations on values conforming to AtomicRepresentable. Use atomics for simple counters, flags, and compare-and-swap patterns where a full lock would be overkill.
Documentation: sosumi.ai/documentation/synchronization/atomic
Counter Example
import Synchronization
final class RequestTracker: Sendable {
let activeRequests = Atomic<Int>(0)
func beginRequest() {
activeRequests.wrappingAdd(1, ordering: .relaxed)
}
func endRequest() {
activeRequests.wrappingSubtract(1, ordering: .relaxed)
}
var count: Int {
activeRequests.load(ordering: .relaxed)
}
}Boolean Flag
let isShutdown = Atomic<Bool>(false)
func shutdown() {
let (exchanged, _) = isShutdown.compareExchange(
expected: false,
desired: true,
ordering: .acquiringAndReleasing
)
guard exchanged else { return } // Already shut down
performCleanup()
}Memory Ordering
Atomic operations require an explicit memory ordering:
| Ordering | Use case |
|---|---|
.relaxed | Counters, statistics — no ordering guarantees needed |
.acquiring | Read that must see all writes before a corresponding release |
.releasing | Write that must be visible to a corresponding acquire |
.acquiringAndReleasing | Compare-and-swap, read-modify-write |
.sequentiallyConsistent | Strongest guarantee — rarely needed |
Guideline: Use .relaxed for simple counters. Use .acquiringAndReleasing for compare-and-swap patterns. Avoid .sequentiallyConsistent unless you have a proven need — it is the most expensive ordering.
When to Use Atomics vs Mutex
- Atomics: Simple scalar values (Int, Bool, UInt64), single-field updates,
counters, flags. Lock-free and very fast.
- Mutex: Compound state (dictionaries, structs with multiple fields),
multi-step operations that must be atomic as a group.
Locks vs Actors: When to Use Each
Use Actors When:
- Async isolation is natural. The protected state is accessed from async
contexts and you can afford the hop.
- Callers can suspend. Actor-isolated APIs are
asyncfrom outside the
actor, so they fit task-based code but not synchronous C callbacks, real-time hooks, or other no-suspension call sites.
- Structured concurrency. You want the compiler to enforce isolation
boundaries and prevent data races statically.
- Most Swift code. Actors are the default recommendation for shared mutable
state in Swift concurrency.
- Complex state with multiple methods. Actor isolation protects all
properties and methods automatically.
// GOOD: Actor for a cache accessed from async contexts
actor ImageDownloader {
private var cache: [URL: UIImage] = [:]
func image(for url: URL) async throws -> UIImage {
if let cached = cache[url] { return cached }
let (data, _) = try await URLSession.shared.data(from: url)
let image = UIImage(data: data)!
cache[url] = image
return image
}
}Use Mutex / Locks When:
- Synchronous access is required. Callers cannot (or should not) be async.
Accessing an actor from synchronous code requires Task and introduces unwanted asynchrony.
- Performance-critical paths. Lock acquisition is nanoseconds; actor hops
involve task scheduling. For tight loops or high-frequency access, a lock may be significantly faster.
- Bridging with C/ObjC. C callbacks, delegate methods, or ObjC APIs that
cannot be made async.
- Simple counters or flags.
Atomic<Int>orAtomic<Bool>is cheaper and
simpler than creating an actor for a single value.
// GOOD: Mutex for synchronous, high-frequency access
final class MetricsCollector: Sendable {
let metrics = Mutex<[String: Int]>([:])
// Called from tight loops, C callbacks, or synchronous code
func increment(_ key: String) {
metrics.withLock { $0[key, default: 0] += 1 }
}
func snapshot() -> [String: Int] {
metrics.withLock { $0 }
}
}Decision Guide
Need shared mutable state protection?
├── Can all access be async?
│ ├── Yes → Use an actor
│ └── No → Use Mutex or OSAllocatedUnfairLock
├── Single scalar value (counter, flag)?
│ └── Use Atomic<Value>
├── Performance-critical (nanosecond-level)?
│ └── Use Mutex or Atomic
└── Bridging C/ObjC callbacks?
└── Use Mutex or OSAllocatedUnfairLockAnti-Patterns
Never put locks inside actors. An actor already serializes access; adding a lock creates double synchronization and risks deadlocks.
// WRONG: Lock inside an actor — double synchronization
actor BadCache {
let lock = Mutex<[String: Data]>([:]) // Unnecessary!
// The actor already protects its state
}
// CORRECT: Just use the actor's built-in isolation
actor GoodCache {
var cache: [String: Data] = [:]
func store(_ data: Data, key: String) {
cache[key] = data
}
}Avoid reaching first for `DispatchSemaphore` or `NSLock` in modern Swift. NSLock is Sendable, but Mutex (iOS 18+) and OSAllocatedUnfairLock (iOS 16+) make the protected state and lock ownership clearer in Swift concurrency code. Use NSLock only when compatibility or existing API shape requires it.
Never hold a lock across `await`. This blocks the thread and can deadlock the cooperative thread pool.
// WRONG: Holding lock across suspension point
mutex.withLock { value in
value = await fetchData() // DEADLOCK RISK
}
// CORRECT: Fetch first, then lock to update
let data = await fetchData()
mutex.withLock { value in
value = data
}Related skills
How it compares
Use swift-concurrency over general Swift docs when you need build-setting-specific answers tied to Swift 6 strict concurrency compiler behavior.
FAQ
When should I use @concurrent?
Add @concurrent on a nonisolated async function when expensive work must run on the concurrent thread pool instead of the caller actor.
What does default MainActor isolation change?
With -default-isolation MainActor, unannotated declarations in the module are inferred @MainActor, reducing annotation burden for UI-bound apps.
How do I handle actor reentrancy?
Do not assume state is unchanged across await. Mutate actor state synchronously before suspension or re-read after await.
Is Swift Concurrency safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.