home / skills / rshankras / claude-code-apple-skills / concurrency

concurrency skill

/skills/swift/concurrency

This skill helps you adopt Swift 6.2 concurrency features like default MainActor inference and @concurrent to reduce data-race errors.

npx playbooks add skill rshankras/claude-code-apple-skills --skill concurrency

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
19.9 KB
---
name: swift-concurrency-updates
description: Swift 6.2 concurrency updates including default MainActor inference, @concurrent for background work, isolated conformances, and approachable concurrency migration. Use when adopting Swift 6.2 concurrency features or fixing data-race errors.
allowed-tools: [Read, Glob, Grep]
---

# Swift 6.2 Concurrency Updates

Swift 6.2 introduces "Approachable Concurrency" -- a set of changes that make strict concurrency dramatically easier to adopt. The philosophy shifts from "opt in to safety" to "safe by default, opt in to concurrency." Code runs on `@MainActor` by default, async functions stay on the calling actor, and you explicitly request background execution with `@concurrent`.

This skill covers only the Swift 6.2 specific changes. For general concurrency patterns (actors, TaskGroup, AsyncSequence, Sendable, cancellation), see the `swift/concurrency-patterns` skill.

## When This Skill Activates

- User is adopting Swift 6.2 concurrency features
- User asks about default MainActor inference or the "infer main actor" build setting
- User encounters data-race errors that Swift 6.2 resolves
- User asks about `@concurrent`, isolated conformances, or approachable concurrency
- User wants to migrate from Swift 6.0/6.1 strict concurrency to 6.2
- User asks how async functions behave differently in Swift 6.2
- User needs to offload CPU-intensive work to a background thread in Swift 6.2

## What Changed in Swift 6.2 vs Before

### The Core Problem with Swift 6.0/6.1

In Swift 6.0 and 6.1, strict concurrency was correct but painful. Developers faced walls of data-race compiler errors that were difficult to resolve. Non-actor-annotated async functions would eagerly hop to the generic concurrent executor, causing unexpected data races when passing mutable state. Conforming `@MainActor` types to non-isolated protocols was often impossible without workarounds.

### Swift 6.2 Changes at a Glance

| Feature | Before (6.0/6.1) | After (6.2) |
|---------|------------------|-------------|
| Default isolation | Nothing inferred; manual `@MainActor` everywhere | Opt-in mode infers `@MainActor` on everything |
| Async function execution | Hops to generic concurrent executor | Stays on calling actor |
| `@MainActor` type conforming to protocol | Compiler error for non-isolated protocols | Isolated conformances: `@MainActor Protocol` |
| Background execution | `Task.detached` or manual nonisolated functions | `@concurrent` attribute |
| Global/static mutable state | Required `@MainActor` annotation or Sendable | Default MainActor mode handles it automatically |

---

## 1. Async Functions Stay on the Calling Actor

In Swift 6.0/6.1, a non-actor-annotated async function called from a `@MainActor` context would hop off the main actor to the generic concurrent executor. This caused data-race errors when the caller passed non-Sendable state.

In Swift 6.2, async functions without specific actor isolation stay on whatever actor they are called from. No hop, no data race.

### Before (Swift 6.0/6.1)

```swift
// ❌ Swift 6.0/6.1 -- ERROR: Sending 'self.processor' risks causing data races
@MainActor
final class StickerModel {
    let processor = PhotoProcessor()

    func extract(_ item: PhotosPickerItem) async throws -> Sticker? {
        let data = try await item.loadTransferable(type: Data.self)
        // processor hops off MainActor -- data race
        return await processor.extractSticker(data: data, with: item.itemIdentifier)
    }
}

class PhotoProcessor {
    func extractSticker(data: Data, with id: String?) async -> Sticker? {
        // This runs on the concurrent executor in 6.0/6.1
        ...
    }
}
```

### After (Swift 6.2)

```swift
// ✅ Swift 6.2 -- No error. extractSticker stays on the caller's actor.
@MainActor
final class StickerModel {
    let processor = PhotoProcessor()

    func extract(_ item: PhotosPickerItem) async throws -> Sticker? {
        let data = try await item.loadTransferable(type: Data.self)
        // processor stays on MainActor -- no data race
        return await processor.extractSticker(data: data, with: item.itemIdentifier)
    }
}

class PhotoProcessor {
    func extractSticker(data: Data, with id: String?) async -> Sticker? {
        // In 6.2, this runs on MainActor because the caller is @MainActor
        ...
    }
}
```

**Why this matters:** Many data-race errors in Swift 6.0/6.1 were caused by this implicit hop. In 6.2, the same code compiles cleanly with no changes needed.

---

## 2. Default MainActor Inference Mode

An opt-in build setting that makes all code implicitly `@MainActor` unless explicitly opted out with `nonisolated`. This eliminates the vast majority of data-race errors for single-threaded app code.

### Enabling It

**Xcode:** Build Settings > Swift Compiler - Concurrency > "Default Actor Isolation" > "MainActor"

**Swift Package Manager:**

```swift
.executableTarget(
    name: "MyApp",
    swiftSettings: [
        .defaultIsolation(MainActor.self)
    ]
)
```

### What Changes

With this mode enabled, you no longer need `@MainActor` annotations on app-level types:

```swift
// ❌ Before (Swift 6.0/6.1) -- manual @MainActor annotations everywhere
@MainActor
final class StickerLibrary {
    static let shared: StickerLibrary = .init()
}

@MainActor
final class StickerModel {
    let processor: PhotoProcessor
    var selection: [PhotosPickerItem]
}

@MainActor
struct ContentView: View {
    @State private var model = StickerModel()
    var body: some View { ... }
}
```

```swift
// ✅ After (Swift 6.2 with default MainActor inference) -- no annotations needed
final class StickerLibrary {
    static let shared: StickerLibrary = .init()  // Implicitly @MainActor
}

final class StickerModel {
    let processor: PhotoProcessor    // Implicitly @MainActor
    var selection: [PhotosPickerItem]
}

struct ContentView: View {
    @State private var model = StickerModel()
    var body: some View { ... }
}
```

### When to Use Default MainActor Inference

| Target Type | Recommended? | Reason |
|-------------|-------------|--------|
| App target | Yes | Apps are UI-driven; most code belongs on MainActor |
| Script / executable | Yes | Scripts are sequential; MainActor default is natural |
| Library / framework | No | Libraries must not impose actor isolation on consumers |
| Package plugin | No | Same reasoning as libraries |

### Opting Out with nonisolated

When a type or function genuinely needs to run off the main actor, mark it `nonisolated`:

```swift
// With "infer main actor" enabled, use nonisolated to opt out:
nonisolated struct ImageProcessor {
    func processImage(_ data: Data) -> UIImage {
        // Runs on any thread, not MainActor
        ...
    }
}

nonisolated func heavyComputation() -> Result {
    // Runs on any thread
    ...
}
```

### Global and Static State Protection

With default MainActor inference enabled, global and static mutable state is automatically protected:

```swift
// ❌ Before -- required explicit annotation or Sendable conformance
@MainActor static let shared: StickerLibrary = .init()

// ✅ After -- default MainActor inference handles it
static let shared: StickerLibrary = .init()  // Implicitly @MainActor
```

Without default MainActor inference, you can still protect individual declarations:

```swift
@MainActor static let shared: StickerLibrary = .init()
```

---

## 3. Isolated Conformances

Allows `@MainActor` types to conform to protocols that do not require actor isolation. Before Swift 6.2, this was a common source of frustrating compiler errors.

### The Problem (Swift 6.0/6.1)

```swift
protocol Exportable {
    func export()
}

@MainActor
final class StickerModel {
    let processor: PhotoProcessor

    func doExport() {
        processor.exportAsPNG()
    }
}

// ❌ Swift 6.0/6.1 -- ERROR: Main actor-isolated conformance crosses isolation boundary
extension StickerModel: Exportable {
    func export() {
        processor.exportAsPNG()  // Needs MainActor, but protocol is non-isolated
    }
}
```

### The Solution (Swift 6.2)

```swift
// ✅ Swift 6.2 -- Isolated conformance
extension StickerModel: @MainActor Exportable {
    func export() {
        processor.exportAsPNG()  // Works: conformance is MainActor-isolated
    }
}
```

### Usage Rules for Isolated Conformances

The compiler enforces that isolated conformances are only used in matching isolation contexts:

```swift
// ✅ Used within @MainActor context -- OK
@MainActor
struct ImageExporter {
    var items: [any Exportable]

    mutating func add(_ item: StickerModel) {
        items.append(item)  // OK: both are @MainActor
    }
}

// ❌ Used outside @MainActor -- compile error
nonisolated struct ImageExporter {
    var items: [any Exportable]

    mutating func add(_ item: StickerModel) {
        items.append(item)  // Error: Main actor-isolated conformance
                            // cannot be used in nonisolated context
    }
}
```

The conformance is not universally available. It only works when the caller shares the same isolation domain.

---

## 4. @concurrent -- Explicit Background Execution

When you need true parallelism for CPU-heavy work, use `@concurrent` to explicitly offload to the background thread pool. This replaces the pattern of using `Task.detached` for compute-intensive operations.

### Basic Usage

```swift
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)  // Background execution
        cachedStickers[id] = sticker
        return sticker
    }

    @concurrent
    static func extractSubject(from data: Data) async -> Sticker {
        // Heavy image processing -- runs on concurrent thread pool
        ...
    }
}
```

### Steps to Offload Work

1. Make the type `nonisolated` (for structs/classes; actors are already isolated)
2. Add `@concurrent` to the function
3. Make the function `async`
4. Callers use `await`

```swift
nonisolated struct ImageProcessor {
    @concurrent
    func resize(image: Data, to size: CGSize) async -> Data {
        // Runs on background thread pool
        ...
    }
}

// Caller (on MainActor):
let resized = await ImageProcessor().resize(image: data, to: targetSize)
```

### @concurrent vs Task.detached vs actor

| Mechanism | Use Case | Structured? |
|-----------|----------|-------------|
| `@concurrent` | Single function that must run on background thread | Yes (inherits task context) |
| `Task.detached` | Fire-and-forget background work, no structured parent | No |
| `actor` | Shared mutable state needing serialized access | N/A (isolation, not scheduling) |
| `Task {}` | Unstructured task inheriting current actor | No |

Prefer `@concurrent` for compute-heavy functions. Prefer actors for shared state. Avoid `Task.detached` when `@concurrent` or structured concurrency works.

### When NOT to Use @concurrent

Do not mark every async function as `@concurrent`. Most app code should stay on the calling actor. Only use `@concurrent` when:

- The function performs CPU-intensive work (image processing, parsing, compression)
- The function performs blocking I/O that would freeze the UI
- You have measured that the work takes enough time to justify the thread hop

```swift
// ❌ Wrong -- trivial work does not need @concurrent
nonisolated struct UserFormatter {
    @concurrent
    func formatName(_ user: User) async -> String {  // Unnecessary thread hop
        return "\(user.firstName) \(user.lastName)"
    }
}

// ✅ Right -- leave it on the calling actor
struct UserFormatter {
    func formatName(_ user: User) -> String {
        return "\(user.firstName) \(user.lastName)"
    }
}
```

---

## 5. Migration Guide

### From Swift 6.0/6.1 to Swift 6.2

**Step 1: Update to Swift 6.2 toolchain**

Ensure your Xcode version supports Swift 6.2 and your project's Swift language version is set to 6.2.

**Step 2: Enable default MainActor inference (for app targets)**

Xcode: Build Settings > Swift Compiler - Concurrency > Default Actor Isolation > MainActor

Swift Package Manager:

```swift
.executableTarget(
    name: "MyApp",
    swiftSettings: [
        .defaultIsolation(MainActor.self)
    ]
)
```

**Step 3: Remove redundant `@MainActor` annotations**

With default MainActor inference enabled, explicit `@MainActor` annotations on app-level types are redundant. Remove them to reduce noise:

```swift
// Before
@MainActor class ViewModel { ... }
@MainActor struct ContentView: View { ... }

// After (with default MainActor inference)
class ViewModel { ... }
struct ContentView: View { ... }
```

**Step 4: Replace `Task.detached` with `@concurrent` where appropriate**

```swift
// Before
func processImages(_ data: [Data]) async -> [UIImage] {
    await withTaskGroup(of: UIImage.self) { group in
        for item in data {
            group.addTask { // Task.detached implied hop
                await self.decode(item)
            }
        }
        return await group.reduce(into: []) { $0.append($1) }
    }
}

// After
@concurrent
func decode(_ data: Data) async -> UIImage {
    // Explicitly runs on background
    ...
}
```

**Step 5: Fix remaining conformance errors with isolated conformances**

```swift
// Before -- workaround with @unchecked Sendable or nonisolated
extension MyModel: @unchecked Sendable {}  // Unsafe workaround

// After -- isolated conformance
extension MyModel: @MainActor Exportable {
    func export() { ... }
}
```

**Step 6: Add `nonisolated` to types/functions that must not be on MainActor**

With default MainActor inference, anything not explicitly marked `nonisolated` runs on MainActor. Audit your code for:

- Background data processing types
- Network parsers
- File I/O utilities
- Computation-heavy algorithms

```swift
nonisolated struct JSONParser {
    @concurrent
    func parse(_ data: Data) async throws -> [Model] { ... }
}
```

### Migration Resources

- Xcode build settings: Swift Compiler > Concurrency
- SwiftSettings API for Swift packages
- Official migration tooling: [swift.org/migration](https://www.swift.org/migration)
- Apple doc reference: `/Users/ravishankar/Downloads/docs/Swift-Concurrency-Updates.md`

---

## Mental Model

```
Swift 6.2 Concurrency Defaults
+--------------------------------------------+
| Everything is @MainActor by default        |
| (with "infer main actor" build setting)    |
|                                            |
| Async functions stay on the calling actor  |
| (no implicit hop to background)            |
|                                            |
| Use @concurrent to explicitly go background|
| Use nonisolated to opt out of MainActor    |
+--------------------------------------------+

Progression:
1. Write code          -> runs on MainActor        -> no data races
2. Use async/await     -> stays on calling actor   -> still no races
3. Need parallelism    -> @concurrent              -> explicit, auditable
4. Need shared state   -> actor                    -> serialized access
```

---

## Top Mistakes

### Mistake 1: Enabling default MainActor inference for a library

```swift
// ❌ Wrong -- library imposes MainActor on all consumers
// Package.swift
.target(
    name: "MyNetworkingLib",
    swiftSettings: [
        .defaultIsolation(MainActor.self)  // Do NOT do this for libraries
    ]
)
```

Libraries should let consumers choose their own isolation strategy. Only app targets and executables should use default MainActor inference.

### Mistake 2: Marking trivial functions @concurrent

```swift
// ❌ Wrong -- unnecessary thread hop for trivial work
@concurrent
func greet(_ name: String) async -> String {
    "Hello, \(name)"
}

// ✅ Right -- no @concurrent needed
func greet(_ name: String) -> String {
    "Hello, \(name)"
}
```

Every `@concurrent` call involves a thread hop. Only use it for genuinely expensive work.

### Mistake 3: Forgetting nonisolated when default MainActor is enabled

```swift
// With default MainActor inference enabled:

// ❌ Wrong -- this CPU-intensive parser now runs on MainActor, blocking UI
struct LargeFileParser {
    func parse(_ data: Data) -> [Record] {
        // Heavy parsing blocks the main thread
        ...
    }
}

// ✅ Right -- opt out of MainActor for background-suitable types
nonisolated struct LargeFileParser {
    @concurrent
    func parse(_ data: Data) async -> [Record] {
        // Runs on background thread pool
        ...
    }
}
```

### Mistake 4: Using isolated conformances in nonisolated contexts

```swift
extension MyModel: @MainActor Exportable {
    func export() { ... }
}

// ❌ Wrong -- trying to use the conformance from a nonisolated context
nonisolated func exportAll(_ items: [any Exportable]) {
    for item in items {
        item.export()  // Compiler error: isolated conformance not available here
    }
}

// ✅ Right -- use the conformance from a matching isolation context
@MainActor
func exportAll(_ items: [any Exportable]) {
    for item in items {
        item.export()  // OK: both are @MainActor
    }
}
```

### Mistake 5: Removing @MainActor annotations without enabling the build setting

```swift
// ❌ Wrong -- removed annotations but did NOT enable default MainActor inference
class ViewModel {  // No longer @MainActor -- state is unprotected
    var items: [Item] = []
    func load() async { ... }
}

// ✅ Right -- either keep annotations OR enable the build setting
// Option A: Keep the annotation
@MainActor
class ViewModel {
    var items: [Item] = []
    func load() async { ... }
}

// Option B: Enable "Default Actor Isolation: MainActor" in build settings
// Then annotations are unnecessary
class ViewModel {
    var items: [Item] = []
    func load() async { ... }
}
```

---

## Review Checklist

When reviewing code that uses or should use Swift 6.2 concurrency features:

### Build Configuration
- [ ] Swift language version is set to 6.2
- [ ] Default MainActor inference is enabled for app/executable targets only (not libraries)
- [ ] Libraries and frameworks do NOT use `.defaultIsolation(MainActor.self)`

### Actor Isolation
- [ ] Redundant `@MainActor` annotations removed (if default MainActor inference is enabled)
- [ ] `nonisolated` applied to types/functions that must run off the main actor
- [ ] CPU-intensive work marked `nonisolated` and uses `@concurrent`
- [ ] No heavy computation running implicitly on MainActor

### @concurrent Usage
- [ ] `@concurrent` only used for genuinely expensive operations
- [ ] `@concurrent` functions are `async`
- [ ] Containing type is `nonisolated` (for structs/classes)
- [ ] `Task.detached` replaced with `@concurrent` where structured concurrency is preferable

### Isolated Conformances
- [ ] `@MainActor Protocol` syntax used for MainActor types conforming to non-isolated protocols
- [ ] Isolated conformances only consumed from matching isolation contexts
- [ ] No `@unchecked Sendable` workarounds that isolated conformances can replace

### Migration Hygiene
- [ ] No leftover `@unchecked Sendable` conformances from pre-6.2 workarounds
- [ ] No unnecessary `Task.detached` calls (prefer `@concurrent` or structured concurrency)
- [ ] Async functions that previously caused data-race errors re-tested without workarounds
- [ ] Global and static mutable state properly protected (via default MainActor or explicit annotation)

---

## Cross-References

- **General concurrency patterns** (actors, TaskGroup, AsyncSequence, Sendable, cancellation): `swift/concurrency-patterns`
- **Actors and isolation deep dive**: `swift/concurrency-patterns/actors-and-isolation.md`
- **Structured concurrency patterns**: `swift/concurrency-patterns/structured-concurrency.md`
- **Swift 6 migration guide**: `swift/concurrency-patterns/migration-guide.md`
- **Apple documentation**: `/Users/ravishankar/Downloads/docs/Swift-Concurrency-Updates.md`

## References

- [Swift Evolution: Approachable Concurrency](https://www.swift.org/blog/approachable-concurrency/)
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/)
- [Swift Concurrency Documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/)

Overview

This skill explains Swift 6.2 concurrency updates focused on approachable concurrency: default MainActor inference, @concurrent for explicit background execution, isolated conformances, and migration tips. It helps developers adopt Swift 6.2 safely and resolve common data-race or conformance errors when moving from Swift 6.0/6.1. Use it as a practical guide to enable the new defaults and pick the right isolation and scheduling primitives.

How this skill works

The skill inspects your concurrency intent and explains how Swift 6.2 changes execution and isolation rules: async functions now stay on the caller's actor, the build setting can make declarations implicitly @MainActor, @concurrent marks functions to run on the background pool, and protocol conformances can be isolated to an actor. It highlights when to add nonisolated, when to mark methods @concurrent, and when isolated conformances are required.

When to use it

  • Adopting Swift 6.2 or migrating from Swift 6.0/6.1
  • Resolving data-race compiler errors caused by implicit executor hops
  • Enabling default MainActor inference for app or executable targets
  • Offloading CPU-heavy or blocking work safely using @concurrent
  • Fixing protocol conformance errors with actor-isolated types

Best practices

  • Enable default MainActor inference for app or script targets, but avoid it for libraries or plugins
  • Use nonisolated for types/functions that must run off MainActor and audit those explicitly
  • Reserve @concurrent for measured, CPU-bound or blocking operations only
  • Prefer @concurrent over Task.detached for structured background work when a single function needs parallelism
  • Use isolated conformances (e.g., extension Type: @MainActor Protocol) to fix cross-isolation protocol errors safely

Example use cases

  • Remove noisy @MainActor annotations across a UI-driven app by enabling default MainActor inference
  • Keep an async helper on the caller actor to avoid data races without code changes
  • Mark a heavy image-processing function @concurrent to run on the background thread pool
  • Make a UI model conform to a protocol using an isolated conformance to avoid compiler errors
  • Audit and mark network parsers or file I/O utilities nonisolated and @concurrent when appropriate

FAQ

When should I enable default MainActor inference?

Enable it for app or executable targets where most code belongs on the main thread; avoid enabling it for libraries or plugins that must remain isolation-agnostic.

How is @concurrent different from Task.detached?

@concurrent declares a single async function to run on the background thread pool while remaining structured and inheriting task context; Task.detached creates an unstructured, fully detached task without parent context.