swift-concurrency

Resolve Swift 6.2 concurrency errors and adopt data-race-safe async patterns. Diagnose and fix actor isolation, Sendable conformance, and strict concurrency compiler diagnostics with a structured triage workflow Apply SE-0466 default MainActor isolation, SE-0461 nonisolated(nonsending), @concurrent functions, and Task.immediate for minimal behavior changes Design actor-based architectures, structured concurrency with TaskGroup and async let, and proper task cancellation patterns Migrate from @preconcurrency imports to full Swift 6 strict concurrency while handling actor reentrancy and cross-isolation callbacks

INSTALLATION
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-concurrency
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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](#triage-workflow)
  • [Swift 6.2 Language Changes](#swift-62-language-changes)
  • [Actor Isolation Rules](#actor-isolation-rules)
  • [Sendable Rules](#sendable-rules)
  • [Structured Concurrency Patterns](#structured-concurrency-patterns)
  • [Task Cancellation](#task-cancellation)
  • [Actor Reentrancy](#actor-reentrancy)
  • [AsyncSequence and AsyncStream](#asyncsequence-and-asyncstream)
  • [@Observable and Concurrency](#observable-and-concurrency)
  • [Synchronization Primitives](#synchronization-primitives)
  • [Common Mistakes](#common-mistakes)
  • [Review Checklist](#review-checklist)
  • [References](#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 (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.

// 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:

  • 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? { /* ... */ }

}

// 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 boot

Actor Isolation Rules

  • All mutable shared state MUST be protected by an actor or global actor.
  • @MainActor for all UI-touching code. No exceptions.
  • Use nonisolated only for methods that access immutable (let) properties

or are pure computations.

  • Use @concurrent to explicitly move work off the caller's actor.
  • Never use nonisolated(unsafe) unless you have proven internal

synchronization and exhausted all other options.

  • Never add manual locks (NSLock, DispatchSemaphore) inside actors.

Sendable Rules

  • Value types (structs, enums) are automatically Sendable when all stored

properties are Sendable.

  • Actors are implicitly Sendable.
  • @MainActor classes are implicitly Sendable. Do NOT add redundant

Sendable conformance.

  • Non-actor classes: must be final with all stored properties let and

Sendable.

  • @unchecked Sendable is a last resort. Document why the compiler cannot

prove safety.

  • Use sending parameters (SE-0430) for finer-grained isolation control.
  • 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.isCancelled or call

try Task.checkCancellation() in loops.

  • Use .task modifier in SwiftUI -- it handles cancellation on view disappear.
  • Use withTaskCancellationHandler for cleanup.
  • Cancel stored tasks in deinit or onDisappear.

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

  • @Observable classes should be @MainActor for view models.
  • Use @State to own an @Observable instance (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+, Synchronization module): Preferred lock for

new code. Stores protected state inside the lock. withLock { } pattern.

  • **OSAllocatedUnfairLock** (iOS 16+, os module): Use when targeting

older iOS versions. Supports ownership assertions for debugging.

  • **Atomic<Value>** (iOS 18+, Synchronization module): 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

  • Blocking the main actor. Heavy computation on @MainActor freezes UI.

Move to a @concurrent function.

  • Unnecessary @MainActor. Network layers, data processing, and model code

do not need @MainActor. Only UI-touching code does.

  • Actors for stateless code. No mutable state means no actor needed. Use a

plain struct or function.

  • Actors for immutable data. Use a Sendable struct, not an actor.
  • Task.detached without good reason. Loses priority, task-local values,

and cancellation propagation.

  • Forgetting task cancellation. Store Task references and cancel them, or

use the .task view modifier.

  • Retain cycles in Tasks. Use [weak self] when capturing self in

long-lived stored tasks.

  • Semaphores in async context. DispatchSemaphore.wait() in async code

will deadlock. Use structured concurrency instead.

  • Split isolation. Mixing @MainActor and nonisolated properties in one

type. Isolate the entire type consistently.

  • MainActor.run instead of static isolation. Prefer @MainActor func

over await MainActor.run { }.

  • 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
  • Sendable conformance is correct (no unjustified @unchecked)
  • Actor reentrancy is handled (no state assumptions across awaits)
  • @preconcurrency imports are documented with removal plan
  • Heavy work uses @concurrent, not @MainActor
  • .task modifier used in SwiftUI instead of manual Task management

References

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card