Chapter 15 — Approachable concurrency (Swift 6.2)

These features require Swift 6.2 (Xcode 26). Everything in Parts I–IV works without them; this chapter is about how 6.2 makes that same model dramatically friendlier to reach.

The problem: concurrency you didn't ask for

@Observable
final class ViewModel {           // NOT annotated — so it's nonisolated
    var items: [Item] = []
    func load() async {
        let fetched = await api.fetch()   // api result crosses back to... where?
        items = fetched                   // ⚠️ isolation errors, Sendable errors
    }
}

Change 1: default actor isolation → MainActor

// With Default Actor Isolation = MainActor, this is implicitly @MainActor:
@Observable
final class ViewModel {           // implicitly @MainActor now
    var items: [Item] = []
    func load() async {
        let fetched = await api.fetch()
        items = fetched           // ✅ just works — we're on the main actor
    }
}
flowchart TB subgraph OLD__Swift_6_0_default__nonisolated___ [\"Swift 6.0 default (nonisolated)\"] O["unannotated code = nonisolated"] --> OE["beginners hit Sendable/isolation
errors on simple code"] end subgraph NEW__Swift_6_2__Default_Actor_Isolation___MainActor___ [\"Swift 6.2 (Default Actor Isolation = MainActor)\"] N["unannotated code = @MainActor"] --> NE["simple code stays on main:
no boundaries, no errors"] NE --> NO["opt INTO concurrency
when you want it"] end

Change 2: nonisolated async runs on the caller's executor

struct S: Sendable {
    func performSync() {}

    // In Swift 6.2, `nonisolated(nonsending)` is the DEFAULT for async:
    func performAsync() async {}      // runs on the caller's executor

    @concurrent
    func alwaysSwitch() async {}      // opts OUT: always runs on the global pool
}

actor MyActor {
    let s = S()
    func call() async {
        s.performSync()          // on the actor's executor
        await s.performAsync()   // ALSO on the actor's executor (stays put)
        await s.alwaysSwitch()   // switches to the global generic executor
    }
}

Change 3: @concurrent to opt into offloading

@concurrent
func processLargeImage(_ data: Data) async -> UIImage {
    // Runs on the global concurrent executor, OFF the caller's actor —
    // so heavy CPU work doesn't block the main actor.
    
}
flowchart LR Q{"Do I want this work
off the current actor?"} Q -->|"No (most code)"| STAY["default: runs on caller's executor
(no boundary, no Sendable friction)"] Q -->|"Yes (heavy/parallel)"| CONC["@concurrent
(explicit offload to global pool)"]

How this reshapes the earlier advice

  • You'll annotate @MainActor far less, because it's the default. The view models in Chapter 11 are
  • You'll hit fewer Sendable errors on ordinary code, because ordinary code stays in one domain (the
  • Concurrency becomes explicit and visible. When you do want parallelism, you write @concurrent,

A caveat for library authors: if you're writing a framework (not an app), main-actor-by-default is often not what you want — libraries should usually stay explicitly nonisolated so callers on any actor can use them. Default-main-actor isolation is primarily an application setting. Know which kind of code you're writing.

Adopting it

What we built in this chapter

  • Diagnosed the Swift 6.0 pain point: nonisolated-by-default forced beginners into Sendable and
  • Default Actor Isolation = MainActor (the "Approachable Concurrency" setting): unannotated code is
  • nonisolated(nonsending) by default (SE-0461): nonisolated async functions now run on the
  • @concurrent: the explicit opt-in to run work off the current actor on the global pool — the
  • How this reshapes app-code advice (annotate @MainActor less, hit fewer Sendable errors,

Mental model to take away

  • Swift 6.2 flips the default posture from "everything is concurrent unless isolated" to
  • Nonisolated async now stays on the caller's executor; you opt into offloading with @concurrent
  • For apps, enable Approachable Concurrency for a gentle on-ramp: write simple main-actor code and
  • None of Parts I–III changes — actors, Sendable, structured concurrency are identical. 6.2 just moves