Claude
Skills
Sign in
Back

swift-concurrency-updates

Included with Lifetime
$97 forever

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.

General

What this skill does


# 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 Isola

Related in General