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
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 (default MainActor isolation) is enabled.
- Strict concurrency checking level (Complete / Targeted / Minimal).
- Determine the current actor context of the code (
@MainActor, custom actor,
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 Sendable or nonisolated(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.
SE-0466: Default MainActor Isolation
With the -default-isolation MainActor compiler flag (or the Xcode 26
"Approachable Concurrency" build setting), all code in a module runs on
@MainActor by default unless explicitly opted out.
Effect: Eliminates most data-race safety errors for UI-bound code and
global/static state without writing @MainActor everywhere.
final class StickerLibrary {
static let shared = StickerLibrary()
var stickers: [Sticker] = []
}
final class StickerModel {
let photoProcessor = PhotoProcessor()
var selection: [PhotosPickerItem] = []
}
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? {
}
}
@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)
}
}
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 {
}
}
To move a function to a background thread:
- Ensure the containing type is
nonisolated (or the function itself is).
- Add
@concurrent to the function.
- Add
async if not already asynchronous.
- 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.
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()
}
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item)
}
}
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.