swift-concurrency

Phase 3 (Implement) and Phase 5 (Review). Load for async patterns during implementation; use checklist during code review. Related: swift-networking for async network calls.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-concurrency" with this command: npx skills add kmshdev/claude-swift-toolkit/kmshdev-claude-swift-toolkit-swift-concurrency

Swift Concurrency

Lifecycle Position

Phase 3 (Implement) and Phase 5 (Review). Load for async patterns during implementation; use checklist during code review. Related: swift-networking for async network calls.

Decision Tree: Where Should This Code Run?

Is it UI code (views, view models, UI state)? → Yes → @MainActor Does it manage shared mutable state? → Yes → Custom actor Is it pure computation with no shared state? → Yes → nonisolated (Swift 6.2: @concurrent for CPU-heavy work) Is it a one-off async operation from synchronous code? → Yes → Task { } (inherits actor context)

Actor Isolation

@MainActor

// On the entire class (recommended for ViewModels) @Observable @MainActor class UserViewModel { var users: [User] = [] var isLoading = false

func loadUsers() async {
    isLoading = true
    defer { isLoading = false }
    users = try? await api.fetchUsers() ?? []
}

}

// On specific methods only class DataProcessor { @MainActor func updateUI(with results: [Result]) { // Safe to touch UI state }

nonisolated func processInBackground(_ data: Data) -> [Result] {
    // Runs on any thread, no actor isolation
}

}

Custom Actors

actor ImageCache { private var cache: [URL: Image] = [:]

func image(for url: URL) -> Image? {
    cache[url]
}

func store(_ image: Image, for url: URL) {
    cache[url] = image
}

}

// Usage — every call crosses an isolation boundary let cache = ImageCache() let image = await cache.image(for: url) // await required

Actor Reentrancy

actor BankAccount { var balance: Decimal

func withdraw(_ amount: Decimal) async throws {
    // WARNING: balance might change between check and deduction
    // because another task could run during the await
    guard balance >= amount else { throw BankError.insufficientFunds }

    await recordTransaction(amount)  // suspension point — other tasks can interleave

    // Re-check invariant after suspension
    guard balance >= amount else { throw BankError.insufficientFunds }
    balance -= amount
}

}

Rule: Always re-validate state after any await inside an actor.

SwiftData Concurrency

@Model classes have specific concurrency constraints enforced by Swift 6:

  • @Model is non-Sendable — do not pass model instances across actor boundaries

  • ModelContext is @MainActor -bound — create and use on the main actor

  • ModelContainer is Sendable — safe to pass between actors

Cross-Actor Access Pattern

// WRONG — passing @Model across actors let item = context.fetch(...) // on MainActor await backgroundActor.process(item) // non-Sendable error

// RIGHT — pass the identifier, re-fetch on the other side let itemID = item.persistentModelID await backgroundActor.process(itemID, container: container)

// In the background actor: let bgContext = ModelContext(container) let item = bgContext.model(for: itemID) as? Item

Testing Implications

When writing tests for @Observable @MainActor models that use SwiftData:

  • Test suites must be @MainActor (matches the model's isolation)

  • Use .serialized on @Suite to prevent parallel container conflicts

  • See ios-testing skill for complete SwiftData testing patterns

Sendability

Sendable Protocol

// Value types are automatically Sendable struct UserID: Sendable { let rawValue: UUID }

// Immutable classes can be Sendable final class Configuration: Sendable { let apiURL: URL // let = immutable = safe let timeout: TimeInterval }

// Mutable shared state needs an actor, not @unchecked Sendable // AVOID unless you truly manage synchronization yourself: final class LegacyCache: @unchecked Sendable { private let lock = NSLock() private var storage: [String: Data] = [:] // Must manually synchronize ALL access }

The sending Keyword (Swift 6)

func process(_ data: sending Data) async { // Compiler guarantees caller won't access data after passing it }

Structured Concurrency

async let (parallel independent work)

async let profile = api.fetchProfile(id) async let posts = api.fetchPosts(userId: id) async let followers = api.fetchFollowers(userId: id)

let (p, po, f) = try await (profile, posts, followers) // all run in parallel

TaskGroup (dynamic parallel work)

let images = try await withThrowingTaskGroup(of: (URL, Image).self) { group in for url in urls { group.addTask { let image = try await downloadImage(from: url) return (url, image) } } var results: [URL: Image] = [:] for try await (url, image) in group { results[url] = image } return results }

Cancellation

func processLargeDataset(_ items: [Item]) async throws { for item in items { try Task.checkCancellation() // Throws if cancelled await process(item) } }

// Or check without throwing if Task.isCancelled { return }

Unstructured Tasks

// Task { } — inherits current actor context @MainActor class ViewModel { func start() { Task { // This runs on MainActor (inherited) await loadData() } } }

// Task.detached { } — no actor inheritance, runs on global executor Task.detached(priority: .background) { // This does NOT run on MainActor let result = heavyComputation(data) await MainActor.run { self.updateUI(result) } }

When to use each:

  • Task { } — 90% of cases. Starting async work from sync context.

  • Task.detached { } — CPU-heavy work that must NOT run on MainActor.

Migration from GCD

GCD Pattern Modern Equivalent

DispatchQueue.main.async { }

@MainActor or MainActor.run { }

DispatchQueue.global().async { }

Task.detached { } or nonisolated method

DispatchGroup

TaskGroup or async let

DispatchSemaphore

Actor isolation (semaphores + async = deadlock risk)

DispatchQueue (serial, for sync) Actor

Completion handler async throws -> return value

Bridging Completion Handlers

func legacyFetch(completion: @escaping (Result<Data, Error>) -> Void) { /* ... */ }

// Wrap with continuation func modernFetch() async throws -> Data { try await withCheckedThrowingContinuation { continuation in legacyFetch { result in continuation.resume(with: result) // MUST be called exactly once } } }

Swift 6 Strict Concurrency

Incremental Adoption

// Enable per-target in Package.swift .target(name: "MyApp", swiftSettings: [.swiftLanguageMode(.v6)])

// Or per-file: suppress specific warnings during migration @preconcurrency import SomeOldLibrary

Common Warnings and Fixes

Warning Fix

"Sending 'value' risks data race" Make type Sendable or use sending parameter

"Non-sendable type captured by @Sendable closure" Move to actor-isolated method or make type Sendable

"Actor-isolated property cannot be mutated from nonisolated context" Add await or annotate caller with same actor

"Global variable is not concurrency-safe" Make it let , move to actor, or use nonisolated(unsafe) (last resort)

"Conformance of X to protocol Y crosses into main actor-isolated code" Use isolated conformance: extension X: @MainActor Y (Swift 6.2+)

Swift 6.2 Approachable Concurrency

Swift 6.2 changes the default: async functions stay on the calling actor instead of hopping to the global concurrent executor. This eliminates entire categories of data race errors for UI-bound code.

Key changes: See references/swift-6-2-changes.md for full details.

Feature What It Does

Async stays on caller's actor No implicit background hop — eliminates "Sending X risks data races" errors

@concurrent attribute Explicit opt-in to run on concurrent pool (replaces Task.detached for this use case)

Main-actor-by-default mode Opt-in: all types in a target implicitly @MainActor

Isolated conformances extension Foo: @MainActor Bar — protocol conformance restricted to main actor

SwiftUI-specific: See references/swiftui-concurrency.md for off-main-thread APIs (Shape , Layout , visualEffect , onGeometryChange ) and their Sendable closure requirements.

Project triage: Before diagnosing concurrency errors, check Xcode Build Settings → Swift Compiler → Concurrency for language version and default actor isolation settings.

Common Patterns

Actor-Isolated Repository

For a complete actor-based persistence pattern with file backing, see swift-actor-persistence .

actor UserRepository { private let api: any APIClientProtocol private var cache: [UserID: User] = [:]

func user(_ id: UserID) async throws -> User {
    if let cached = cache[id] { return cached }
    let user: User = try await api.request("/users/\(id.rawValue)")
    cache[id] = user
    return user
}

}

.task Modifier for View Lifecycle

struct UserView: View { @State private var viewModel = UserViewModel()

var body: some View {
    content
        .task { await viewModel.load() }          // auto-cancels on disappear
        .task(id: selectedID) { await viewModel.load(selectedID) }  // re-runs when ID changes
}

}

Review Checklist

  • No data races: mutable shared state in actors or protected by isolation

  • Actor isolation boundaries clearly documented (which actor owns which state)

  • Cancellation cooperative: long operations check Task.checkCancellation()

  • Task references stored for cleanup if not using .task modifier

  • .task modifier used for view lifecycle async work (not onAppear { Task { } } )

  • @MainActor on view models and UI state classes

  • No DispatchSemaphore mixed with async code (deadlock risk)

  • withCheckedContinuation resumes exactly once (not zero, not twice)

  • State re-validated after suspension points in actors (reentrancy)

  • @unchecked Sendable justified in comment if used

  • Swift 6.2: Check project concurrency settings before diagnosing (language version, default actor isolation)

  • Swift 6.2: @concurrent used instead of Task.detached for explicit background work

  • SwiftUI: No @MainActor state accessed from Sendable closures (visualEffect, onGeometryChange) — use value copies

Cross-References

  • swift-networking — async/await network patterns

  • swiftui-ui-patterns — .task modifier, @MainActor ViewModel pattern, Sendable checks

  • ios-testing — testing async code with Swift Testing (#expect with await )

  • code-analyzer — concurrency safety review section

  • swift-actor-persistence — actor-based persistence pattern with file backing (extends the Actor-Isolated Repository pattern above)

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

swiftui-26-api

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

swiftui-colors-api

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

swift-actor-persistence

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

swiftui-view-refactor

No summary provided by upstream source.

Repository SourceNeeds Review