# Approachable Concurrency Patterns

Swift 6.2 lets a target start out effectively single-threaded: with both
Default Actor Isolation set to `MainActor` and Approachable Concurrency turned
on, code runs on the main actor until you introduce parallelism deliberately.
Either setting alone gives only part of that picture.

## What problem 6.2 fixed

Swift 6.0 and 6.1 already enforced data-race safety at compile time, but
straightforward code tripped it. An async method on a type with mutable state
left the caller's actor and ran on the global concurrent executor, so passing
state into it counted as sending across isolation even though nobody wanted
parallel execution.

```swift
final class WaveformRenderer {
    var sampleRate = 44_100.0
    func render(_ audio: Data) async -> Waveform { Waveform(samples: []) }
}

@MainActor
final class EpisodeDetailModel {
    let renderer = WaveformRenderer()
    var waveform: Waveform?

    func refresh(with audio: Data) async {
        waveform = await renderer.render(audio)
        // Swift 6.0 / 6.1: error: sending 'self.renderer' risks causing data races
        // Swift 6.2 with NonisolatedNonsendingByDefault: compiles, render runs on the main actor
    }
}
```

## Default MainActor isolation (SE-0466)

Enable it with one of:

- `-default-isolation MainActor` passed to the compiler,
- `.defaultIsolation(MainActor.self)` in a SwiftPM target's `swiftSettings`,
- Xcode build setting `Default Actor Isolation` = `MainActor`.

This is a different switch from Xcode's `Approachable Concurrency` setting. That
one enables a bundle of upcoming features: `NonisolatedNonsendingByDefault`,
inference of isolated conformances, `Sendable` inference for captures, and
smaller global-actor usability changes. In Swift 6 language mode some of those
are already on, so the setting mainly adds the first two.

With default isolation on:

- global and `static` variables are protected by the main actor,
- protocol conformances are isolated to the main actor,
- UI code that never runs in parallel needs almost no annotations.

```swift
final class DownloadSettings {
    static let shared = DownloadSettings()
    var wifiOnly = true
}

final class LibraryModel {
    let renderer = WaveformRenderer()
    var selection: [Episode.ID] = []
}

extension LibraryModel: Archivable {
    func archive() -> Data { Data() }
}
```

None of these declarations says `@MainActor`, yet all of them are main-actor
isolated. Choose this mode for apps, command-line tools and other executables
dominated by UI or sequential logic. Keep it off for libraries that callers
should be able to use from any isolation.

## Async functions run on the caller (SE-0461)

When `NonisolatedNonsendingByDefault` is enabled, a `nonisolated` async function
inherits its caller's actor; the explicit spelling is `nonisolated(nonsending)`.
Arguments never leave the caller's isolation, so the "risks causing data races"
errors above disappear with no annotation. To run on the thread pool instead,
mark the function `@concurrent`.

## Leaving the caller with `@concurrent`

A pattern that keeps the cache on the caller's actor and sends only the heavy
work to the pool:

```swift
@MainActor
final class ThumbnailStore {
    private var cache: [URL: Thumbnail] = [:]

    func thumbnail(for url: URL) async throws -> Thumbnail {
        if let cached = cache[url] { return cached }
        let made = try await Self.decode(url)
        cache[url] = made
        return made
    }

    @concurrent
    nonisolated private static func decode(_ url: URL) async throws -> Thumbnail {
        let bytes = try Data(contentsOf: url)
        return Thumbnail(byteCount: bytes.count)
    }
}

struct Thumbnail: Sendable { let byteCount: Int }
```

The four steps are always the same: make the type or function `nonisolated`,
add `@concurrent`, make it `async` if needed, and `await` it at each call site.

## Starting without a queue hop (SE-0472)

`Task.immediate { }` begins executing synchronously on the current actor and
only suspends at its first `await`, where a plain `Task { }` would be enqueued
and start later. Reach for it when that enqueue delay is visible, such as
reacting to a keystroke. `Task.immediateDetached { }` does the same without
inheriting context. Both need the OS 26 runtime.

## Isolated conformances

```swift
protocol Archivable {
    func archive() -> Data
}

@MainActor
final class PlaylistModel {
    var titles: [String] = []
}

extension PlaylistModel: @MainActor Archivable {
    func archive() -> Data { Data(titles.joined(separator: "\n").utf8) }
}

@MainActor
struct BackupBatch {
    var items: [any Archivable] = []
    mutating func include(_ playlist: PlaylistModel) {
        items.append(playlist)            // allowed: same isolation
    }
}

nonisolated struct OffMainBackupBatch {
    var items: [any Archivable] = []
    mutating func include(_ playlist: PlaylistModel) {
        items.append(playlist)
        // error: main actor-isolated conformance of 'PlaylistModel' to 'Archivable'
        // cannot be used in nonisolated context
    }
}
```

## Immutable weak references (SE-0481)

`weak let` declares a weak reference that is never reassigned. Because the
binding cannot change, a type storing only `weak let` references to `Sendable`
objects can itself be `Sendable`. Swift 6.3 only.

```swift
final class SessionObserver: Sendable {
    weak let session: PlaybackSession?
    init(session: PlaybackSession) { self.session = session }
}
```

## Transactional observation (SE-0475)

```swift
for await pending in Observations({ uploads.pendingCount }) {
    badge.update(pending)
}
```

Each iteration sees a consistent snapshot of the properties the closure read.

## Global and static state

Globals and statics are shared by definition, so they race unless isolated. The
usual answer is the main actor:

```swift
@MainActor
final class FeatureFlags {
    static let shared = FeatureFlags()
    var experiments: Set<String> = []
}
```

Under SE-0466 the `@MainActor` is inferred and can be deleted.

## Build settings and migration

- Every approachable-concurrency feature is opt-in.
- Xcode 26: Build Settings, section Swift Compiler - Concurrency.
- SwiftPM: `swiftSettings` on each target, through the `SwiftSetting` API.

```swift
.target(
    name: "PlayerApp",
    swiftSettings: [
        .defaultIsolation(MainActor.self),
        .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
        .enableUpcomingFeature("InferIsolatedConformances")
    ]
)
```

- Swift 6 language mode means complete checking with data-race errors.
  Targeted and Minimal exist only for Swift 5 targets preparing to switch.
- Swift 6.2 ships migration tooling that can apply many of the required edits
  automatically; see swift.org/migration.

## The progression

1. Start with main-actor-by-default code: nothing runs in parallel, so nothing
   races.
2. Let async functions run where they are called: still no parallelism.
3. When profiling shows a need, move specific functions to the pool with
   `@concurrent`.
