All skills
avdlee avatar

/swift-concurrency

@35095d4

Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compiler and linter warnings.

Use this Skill: https://skilld.dev/gh/avdlee/swift-concurrency-agent-skill/swift-concurrency

This session only. Nothing lands on disk.

referencesactors.md

≈4.4k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Actors

Use this when:

  • You need to protect class-based mutable state from concurrent access.
  • You are choosing between actor, @MainActor, nonisolated, or Mutex.
  • You are resolving protocol conformance issues on actor-isolated types.

Skip this file if:

  • You mainly need to make a value safe to transfer across boundaries. Use sendable.md.
  • You are debugging execution threads or suspension behavior. Use threading.md.

Jump to:

  • Actor Isolation
  • Global Actors / @MainActor
  • Isolated vs Nonisolated
  • Actor Reentrancy
  • Isolated Deinit / Isolated Conformances (Swift 6.2+)
  • #isolation Macro
  • Mutex: Alternative to Actors
  • Decision Tree

What is an Actor?

Actors protect mutable state by ensuring only one task accesses it at a time. They're reference types with automatic synchronization.

actor Counter {
    var value = 0
    
    func increment() {
        value += 1
    }
}

Key guarantee: Only one task can access mutable state at a time (serialized access).

Course Deep Dive: This topic is covered in detail in Lesson 5.1: Understanding actors in Swift Concurrency

Actor Isolation

Enforced by compiler

actor BankAccount {
    var balance: Int = 0
    
    func deposit(_ amount: Int) {
        balance += amount
    }
}

let account = BankAccount()
account.balance += 1 // ❌ Error: can't mutate from outside
await account.deposit(1) // ✅ Must use actor's methods

Reading properties

let account = BankAccount()
await account.deposit(100)
print(await account.balance) // Must await reads too

Always use await when accessing actor properties/methods—you don't know if another task is inside.

Actors vs Classes

Similarities

  • Reference types (copies share same instance)
  • Can have properties, methods, initializers
  • Can conform to protocols

Differences

  • No inheritance (except NSObject for Objective-C interop)
  • Automatic isolation (no manual locks needed)
  • Implicit Sendable conformance
// ❌ Can't inherit from actors
actor Base {}
actor Child: Base {} // Error

// ✅ NSObject exception
actor Example: NSObject {} // OK for Objective-C

Global Actors

Shared isolation domain across types, functions, and properties.

@MainActor

Ensures execution on main thread:

@MainActor
final class ViewModel {
    var items: [Item] = []
}

@MainActor
func updateUI() {
    // Always runs on main thread
}

@MainActor
var title: String = ""

Custom global actors

@globalActor
actor ImageProcessing {
    static let shared = ImageProcessing()
    private init() {} // Prevent duplicate instances
}

@ImageProcessing
final class ImageCache {
    var images: [URL: Data] = [:]
}

@ImageProcessing
func applyFilter(_ image: UIImage) -> UIImage {
    // All image processing serialized
}

Use private init to prevent creating multiple executors.

Course Deep Dive: This topic is covered in detail in Lesson 5.2: An introduction to Global Actors

@MainActor Best Practices

When to use

UI-related code that must run on main thread:

@MainActor
final class ContentViewModel: ObservableObject {
    @Published var items: [Item] = []
}

Replacing DispatchQueue.main

// Old way
DispatchQueue.main.async {
    // Update UI
}

// Modern way
await MainActor.run {
    // Update UI
}

// Better: Use attribute
@MainActor
func updateUI() {
    // Automatically on main thread
}

MainActor.assumeIsolated

Use sparingly - assumes you're on main thread, crashes if not:

func methodB() {
    assert(Thread.isMainThread) // Validate assumption
    
    MainActor.assumeIsolated {
        someMainActorMethod()
    }
}

Prefer: Explicit @MainActor or await MainActor.run over assumeIsolated.

Course Deep Dive: This topic is covered in detail in Lesson 5.3: When and how to use @MainActor

Isolated vs Nonisolated

Default: Isolated

Actor methods are isolated by default:

actor BankAccount {
    var balance: Double
    
    // Implicitly isolated
    func deposit(_ amount: Double) {
        balance += amount
    }
}

Isolated parameters

Reduce suspension points by inheriting caller's isolation:

struct Charger {
    static func charge(
        amount: Double,
        from account: isolated BankAccount
    ) async throws -> Double {
        // No await needed - we're isolated to account
        try account.withdraw(amount: amount)
        return account.balance
    }
}

Isolated closures

actor Database {
    func transaction<T>(
        _ operation: @Sendable (_ db: isolated Database) throws -> T
    ) throws -> T {
        beginTransaction()
        let result = try operation(self)
        commitTransaction()
        return result
    }
}

// Usage: Multiple operations, one await
try await database.transaction { db in
    db.insert(item1)
    db.insert(item2)
    db.insert(item3)
}

Generic isolated extension

extension Actor {
    func performInIsolation<T: Sendable>(
        _ block: @Sendable (_ actor: isolated Self) throws -> T
    ) async rethrows -> T {
        try block(self)
    }
}

// Usage
try await bankAccount.performInIsolation { account in
    try account.withdraw(amount: 20)
    print("Balance: \(account.balance)")
}

Nonisolated

Opt out of isolation for immutable data:

actor BankAccount {
    let accountHolder: String
    
    nonisolated var details: String {
        "Account: \(accountHolder)"
    }
}

// No await needed
print(account.details)

Protocol conformance

extension BankAccount: CustomStringConvertible {
    nonisolated var description: String {
        "Account: \(accountHolder)"
    }
}

Course Deep Dive: This topic is covered in detail in Lesson 5.4: Isolated vs. non-isolated access in actors

Isolated Deinit (Swift 6.2+)

Clean up actor state on deallocation:

actor FileDownloader {
    var downloadTask: Task<Void, Error>?
    
    isolated deinit {
        downloadTask?.cancel() // Can call isolated methods
    }
}

Requires: iOS 18.4+, macOS 15.4+

Course Deep Dive: This topic is covered in detail in Lesson 5.5: Using Isolated synchronous deinit

Global Actor Isolated Conformance (Swift 6.2+)

Protocol conformance respecting actor isolation:

@MainActor
final class PersonViewModel {
    let id: UUID
    var name: String
}

extension PersonViewModel: @MainActor Equatable {
    static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
        lhs.id == rhs.id && lhs.name == rhs.name
    }
}

Enable: InferIsolatedConformances upcoming feature.

Course Deep Dive: This topic is covered in detail in Lesson 5.6: Adding isolated conformance to protocols

SendableMetatype Error with Isolated Conformances

Isolated conformances cannot satisfy a SendableMetatype requirement. This surfaces when you pass MyClass.self to a generic function whose type parameter requires Sendable.

protocol P {
    static func doSomething()
}

func doSomethingStatic<T: P & SendableMetatype>(_ type: T.Type) { }  // explicitly requires a Sendable type/metatype

@MainActor
class C { }

extension C: @MainActor P {
    static func doSomething() { }
}

@MainActor
func test(c: C) {
    doSomethingStatic(C.self)
    // ❌ main actor-isolated conformance of 'C' to 'P' cannot satisfy
    //    conformance requirement for a 'Sendable' type parameter
}

Fix options:

  1. Remove actor isolation from the original conformance if the protocol requirements don't access actor state:
@MainActor
class C: P {
    nonisolated static func doSomething() { }  // ✅ Non-isolated requirement on a non-isolated conformance
}
  1. Avoid passing the metatype across isolation boundaries — call the static method directly rather than routing through the generic function.

  2. Make the generic function actor-aware so it accepts an isolated conformance (requires changing the callee's signature).

Actor Reentrancy

Critical: State can change between suspension points.

actor BankAccount {
    var balance: Double
    
    func deposit(amount: Double) async {
        balance += amount
        
        // ⚠️ Actor unlocked during await
        await logActivity("Deposited \(amount)")
        
        // ⚠️ Balance may have changed!
        print("Balance: \(balance)")
    }
}

Problem

async let _ = account.deposit(50)
async let _ = account.deposit(50)
async let _ = account.deposit(50)

// May print same balance three times:
// Balance: 150
// Balance: 150
// Balance: 150

Solution

Complete actor work before suspending:

func deposit(amount: Double) async {
    balance += amount
    print("Balance: \(balance)") // Before suspension
    
    await logActivity("Deposited \(amount)")
}

Rule: Don't assume state is unchanged after await.

Course Deep Dive: This topic is covered in detail in Lesson 5.7: Understanding actor reentrancy

#isolation Macro

Inherit caller's isolation for generic code:

extension Collection where Element: Sendable {
    func sequentialMap<Result: Sendable>(
        isolation: isolated (any Actor)? = #isolation,
        transform: (Element) async -> Result
    ) async -> [Result] {
        var results: [Result] = []
        for element in self {
            results.append(await transform(element))
        }
        return results
    }
}

// Usage from @MainActor context
Task { @MainActor in
    let names = ["Alice", "Bob"]
    let results = await names.sequentialMap { name in
        await process(name) // Inherits @MainActor
    }
}

Benefits: Avoids unnecessary suspensions, allows non-Sendable data.

Task Closures and Isolation Inheritance

When spawning unstructured Task closures that need to work with non-Sendable types, you must capture the isolation parameter to inherit the caller's isolation context.

Problem: Task closures are @Sendable, which prevents capturing non-Sendable types:

func process(delegate: NonSendableDelegate) {
  Task {
    delegate.doWork() // ❌ Error: capturing non-Sendable type
  }
}

Solution: Use #isolation parameter and capture it inside the Task:

func process(
  delegate: NonSendableDelegate,
  isolation: isolated (any Actor)? = #isolation
) {
  Task {
    _ = isolation  // Forces capture, Task inherits caller's isolation
    delegate.doWork()  // ✅ Safe - running on caller's actor
  }
}

Why _ = isolation is required: Per SE-0420, Task closures only inherit isolation when "a non-optional binding of an isolated parameter is captured by the closure." The _ = isolation statement forces this capture. The capture list syntax [isolation] should work but currently does not.

When to use this pattern:

  • Spawning Tasks that work with non-Sendable delegate objects
  • Fire-and-forget async work that needs access to caller's state
  • Bridging callback-based APIs to async streams while keeping delegates alive

Note: This pattern keeps the non-Sendable value alive and accessible within the Task. The Task runs on the caller's isolation domain, so no cross-isolation "sending" occurs.

Course Deep Dive: This topic is covered in detail in Lesson 5.8: Inheritance of actor isolation using the #isolation macro

Custom Actor Executors

Advanced: Control how actor schedules work.

Serial executor

final class DispatchQueueExecutor: SerialExecutor {
    private let queue: DispatchQueue
    
    init(queue: DispatchQueue) {
        self.queue = queue
    }
    
    func enqueue(_ job: consuming ExecutorJob) {
        let unownedJob = UnownedJob(job)
        let executor = asUnownedSerialExecutor()
        
        queue.async {
            unownedJob.runSynchronously(on: executor)
        }
    }
}

actor LoggingActor {
    private let executor: DispatchQueueExecutor
    
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        executor.asUnownedSerialExecutor()
    }
    
    init(queue: DispatchQueue) {
        executor = DispatchQueueExecutor(queue: queue)
    }
}

When to use

  • Integration with legacy DispatchQueue-based code
  • Specific thread requirements (e.g., C++ interop)
  • Custom scheduling logic

Default executor is usually sufficient.

Course Deep Dive: This topic is covered in detail in Lesson 5.9: Using a custom actor executor

Mutex: Alternative to Actors

Synchronous locking without async/await overhead (iOS 18+, macOS 15+).

Basic usage

import Synchronization

final class Counter {
    private let count = Mutex<Int>(0)
    
    var currentCount: Int {
        count.withLock { $0 }
    }
    
    func increment() {
        count.withLock { $0 += 1 }
    }
}

Sendable access to non-Sendable types

final class TouchesCapturer: Sendable {
    let path = Mutex<NSBezierPath>(NSBezierPath())
    
    func storeTouch(_ point: NSPoint) {
        path.withLock { path in
            path.move(to: point)
        }
    }
}

Error handling

func decrement() throws {
    try count.withLock { count in
        guard count > 0 else {
            throw Error.reachedZero
        }
        count -= 1
    }
}

Mutex vs Actor

Feature Mutex Actor
Synchronous ✅ ❌ (requires await)
Async support ❌ ✅
Thread blocking ✅ ❌ (cooperative)
Fine-grained locking ✅ ❌ (whole actor)
Legacy code integration ✅ ❌

Use Mutex when:

  • Need synchronous access
  • Working with legacy non-async APIs
  • Fine-grained locking required
  • Low contention, short critical sections

Use Actor when:

  • Can adopt async/await
  • Need logical isolation
  • Working in async context

Course Deep Dive: This topic is covered in detail in Lesson 5.10: Using a Mutex as an alternative to actors

Common Patterns

View model with @MainActor

@MainActor
final class ContentViewModel: ObservableObject {
    @Published var items: [Item] = []
    
    func loadItems() async {
        items = try await api.fetchItems()
    }
}

Background processing with custom actor

@ImageProcessing
final class ImageProcessor {
    func process(_ images: [UIImage]) async -> [UIImage] {
        images.map { applyFilters($0) }
    }
}

Mixed isolation

actor DataStore {
    private var items: [Item] = []
    
    func add(_ item: Item) {
        items.append(item)
    }
    
    nonisolated func itemCount() -> Int {
        // ❌ Can't access items
        return 0
    }
}

Transaction pattern

actor Database {
    func transaction<T>(
        _ operation: @Sendable (_ db: isolated Database) throws -> T
    ) throws -> T {
        beginTransaction()
        defer { commitTransaction() }
        return try operation(self)
    }
}

Best Practices

  1. Prefer actors over manual locks for async code
  2. Use @MainActor for UI - all view models, UI updates
  3. Minimize work in actors - keep critical sections short
  4. Watch for reentrancy - don't assume state unchanged after await
  5. Use nonisolated sparingly - only for truly immutable data
  6. Avoid assumeIsolated - prefer explicit isolation
  7. Custom executors are rare - default is usually best
  8. Consider Mutex for sync code - when async overhead not needed
  9. Complete actor work before suspending - prevent reentrancy bugs
  10. Use isolated parameters - reduce suspension points

Decision Tree

Need thread-safe mutable state?
├─ Async context?
│  ├─ Single instance? → Actor
│  ├─ Global/shared? → Global Actor (@MainActor, custom)
│  └─ UI-related? → @MainActor
│
└─ Synchronous context?
   ├─ Can refactor to async? → Actor
   ├─ Legacy code integration? → Mutex
   └─ Fine-grained locking? → Mutex

Further Learning

For migration strategies, advanced patterns, and real-world examples, see Swift Concurrency Course.

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive instructions and reference material for diagnosing and fixing Swift Concurrency issues. It includes detailed guidance on Swift 6 migration, actor isolation, and task management. No security risks or malicious patterns were detected.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer7mo

    1/16 files flagged

  • ZeroLeaks5mo

    Score: 93/100 · 2 sections analyzed

Signed by skilld at 35095d4. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 2 months ago
  • swift
  • concurrency
  • async-await
  • actors
  • sendable
  • main-actor
  • swift-6
  • migration
  • data-races
  • thread-safety

README badge

README badge for avdlee/swift-concurrency-agent-skill

Diagnose and fix Swift Concurrency issues including data races, isolation violations, and Sendable conformance problems. Refactors callback-based code to async/await and guides Swift 6 migration by analyzing project settings, determining isolation boundaries, and applying the smallest safe changes.

Generated from the current SKILL.md.

Does this skill help with Swift 6 migration?
Yes. The skill guides Swift 6 migration by analyzing language mode and strict concurrency settings from Package.swift or .pbxproj, then walking through diagnostics related to tasks, actors, @MainActor, Sendable, and data races.
What project settings does this skill check?
The skill analyzes Swift language mode, strict concurrency level, default isolation, and upcoming features from both SwiftPM (Package.swift) and Xcode (.pbxproj) to determine the correct fix for each diagnostic.
Does this skill refactor callback-based code to async/await?
Yes. The skill helps refactor legacy callback patterns to async/await, and routes to references/migration.md for detailed closure-to-async conversion strategies.
Will this skill recommend @MainActor as a blanket fix?
No. The skill requires justification for @MainActor isolation and avoids recommending it unless the code is truly UI-bound; it also favors structured concurrency over unstructured tasks.
Does this skill handle Core Data concurrency issues?
Yes. The skill addresses NSManagedObject sendability warnings and crossing context/actor boundaries, with routes to references/core-data.md for detailed solutions.

Generated from the current SKILL.md. These answers refresh after source changes.