# Swift 6 Concurrency Migration Playbook: Moving a Swift 5 Codebase to Strict Concurrency

Source: https://devdocs10.com/articles/swift/swift-6-concurrency-migration  
By Murat Bo · Published 2026-10-17 · DevDocs10 (https://devdocs10.com)

The Swift 6 language mode turns data-race safety checks into compile errors. This playbook shows how to get an existing Swift 5 codebase there one module at a time: how to switch the checks on, which order to fix things in, and the real fixes for each diagnostic you will hit.

> **Quick answer:** Migrate one module at a time. Leave the module in the Swift 5 language mode and turn on complete concurrency checking (`SWIFT_STRICT_CONCURRENCY = complete` in Xcode, `.enableUpcomingFeature("StrictConcurrency")` in SwiftPM), so every problem shows up as a warning while the build still works. Fix the warnings in a fixed order: global state, `Sendable` types, actor isolation, protocol conformances, then dependencies. Only then switch that module to Swift 6 (`SWIFT_VERSION = 6` or `.swiftLanguageMode(.v6)`).

> **Difficulty:** Advanced
>
> **You'll understand this better if you know:** [async/await](/articles/swift/swift-async-await), [actors](/articles/swift/swift-actors), and [Sendable and @MainActor isolation](/articles/swift/swift-sendable-mainactor-isolation). For the bigger picture of Swift 4 → 5 → 6 upgrades, see [Swift Migration Guides](/articles/swift/swift-migration-guides).

## 1. What Changes in the Swift 6 Language Mode

Keep three separate things apart: the **compiler version** (the toolchain that ships with Xcode), the **language mode** (`-swift-version 5` or `6`), and your **deployment target**. A Swift 6 compiler builds Swift 5 mode code without complaint, and you choose the language mode *per module*. Your app target can stay in Swift 5 mode while a framework it links already uses Swift 6, and the other way around.

Switching a module to Swift 6 mode does two things:

- It turns on complete data-race checking (actor isolation and `Sendable` checks across the whole module) and reports what it finds as **errors**.
- It turns on every upcoming feature planned for Swift 6. The concurrency-related ones are `StrictConcurrency`, `GlobalConcurrency`, `RegionBasedIsolation`, `InferSendableFromCaptures`, `DisableOutwardActorInference`, `IsolatedDefaultValues`, `DynamicActorIsolation` and `GlobalActorIsolatedTypesUsability`. Run `swiftc -print-supported-features` to see the full list for your toolchain.

| Configuration | What you get |
| --- | --- |
| Swift 5 mode, default (`minimal`) | `Sendable` checks only where code explicitly adopted `Sendable`. Actor-isolation checks only in code that uses concurrency features. |
| Swift 5 mode + `targeted` | `Sendable` and actor-isolation checks in all code that has adopted concurrency (`async`, actors, `Sendable`) |
| Swift 5 mode + `complete` | The Swift 6 checks across the whole module, as warnings ending in "this is an error in the Swift 6 language mode" |
| Swift 6 mode | Complete checking as errors, plus runtime isolation checks (section 11) |

> **Note:** Swift 6 mode also adds *runtime* checks. If a `@MainActor` closure gets called on a background thread by code that was not checked, the app now traps instead of quietly racing. Plan time for this. Section 11 shows the crash and its fix.

## 2. Prerequisites and Versions

| You want | Requires |
| --- | --- |
| Swift 6 language mode, `sending`, region-based isolation | Swift 6.0 compiler (Xcode 16) or later |
| Per-target `.swiftLanguageMode(.v5)` in a manifest | `swift-tools-version: 6.0` or later |
| Isolated conformances (`extension T: @MainActor P`), default `MainActor` isolation, `@concurrent`, `swift package migrate` | Swift 6.2 compiler (Xcode 26) or later |
| `Mutex` from the Synchronization module | Runtime OS: macOS 15, iOS 18, tvOS 18, watchOS 11, visionOS 2 or later |

Do this before you turn on any checks:

- Make sure the project builds cleanly in Swift 5 mode and that all tests pass. That clean build is your baseline.
- Update dependencies. Many libraries have since added `Sendable` annotations, and every one of those removes warnings from your side.
- Run CI on the same Xcode version you use locally. Concurrency diagnostics change between compiler releases.
- You do not need to raise your deployment target. The language mode and the deployment target are independent. Only specific APIs such as `Mutex` have OS requirements.

## 3. Turn On Checking Incrementally

### Xcode build settings

| Build setting (UI name) | Key | Values |
| --- | --- | --- |
| Swift Language Version | `SWIFT_VERSION` | `5.0`, `6.0` |
| Strict Concurrency Checking | `SWIFT_STRICT_CONCURRENCY` | `minimal`, `targeted`, `complete` (applies to Swift 5 mode only; Swift 6 mode always uses complete) |
| Default Actor Isolation | `SWIFT_DEFAULT_ACTOR_ISOLATION` | `nonisolated`, `MainActor` (Xcode 26+) |
| Approachable Concurrency | `SWIFT_APPROACHABLE_CONCURRENCY` | `YES` / `NO` (Xcode 26+) |

Change these per target, not per project. An xcconfig file makes the state of each target easy to review in code:

```swift
// Networking.xcconfig: still Swift 5, all data-race issues as warnings
SWIFT_VERSION = 5.0
SWIFT_STRICT_CONCURRENCY = complete

// Core.xcconfig: migrated
SWIFT_VERSION = 6.0
```

### Swift Package Manager

With `swift-tools-version: 6.0`, every target defaults to Swift 6 mode. Targets that are not ready opt back into Swift 5 mode and turn on complete checking as warnings:

```swift
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
    name: "Shop",
    platforms: [.macOS(.v15)],
    targets: [
        // Migrated: uses the package-wide Swift 6 mode
        .target(name: "Core"),

        // Not migrated yet: Swift 5 mode, complete checking as warnings
        .target(
            name: "Legacy",
            swiftSettings: [
                .swiftLanguageMode(.v5),
                .enableUpcomingFeature("StrictConcurrency")
            ]
        ),
        .executableTarget(name: "App", dependencies: ["Core", "Legacy"])
    ],
    swiftLanguageModes: [.v6]
)
```

On 5.9 or 5.10 tools, use `.enableExperimentalFeature("StrictConcurrency")` instead. Manifests with tools version 6.0 or later use `swiftLanguageModes`, which replaces `swiftLanguageVersions` ([SE-0441](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0441-formalize-language-mode-terminology.md)).

### Command line

To see how many warnings you would get, without editing any files:

```bash
swift build -Xswiftc -strict-concurrency=complete
swift build -Xswiftc -swift-version -Xswiftc 6
swiftc -swift-version 6 -parse-as-library Sources/Core/*.swift -typecheck
```

If complete checking floods you with warnings, the [Swift migration guide](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/migrationstrategy) suggests a gentler start in Swift 5 mode. Turn on one upcoming feature at a time: `GlobalConcurrency`, `InferSendableFromCaptures` or `DisableOutwardActorInference`. Then move to complete checking.

## 4. The Order of Work

1. **Baseline.** Get a clean Swift 5 build with all tests green on the current Xcode, and commit it.
2. **Pick a module.** The migration guide suggests starting with the outermost module, usually the app target, because nothing depends on it and changes stay local. If many warnings trace back to one shared dependency you own, fix that dependency's global state and `Sendable` conformances first.
3. **Turn on complete checking as warnings** for that module only (section 3).
4. **Fix by category, in this order:** global and static mutable state (section 5),
5. value types that should be `Sendable` (section 6),
6. UI types and other main-thread code: add `@MainActor` (section 7),
7. protocol conformances that cross isolation (section 8),
8. values sent between tasks and actors (section 9),
9. imports of modules you do not control (section 10).

Each category removes warnings from the ones after it. Marking a type Sendable early, for example, makes many later "sending risks causing data races" warnings disappear.Describe what is already true. Don't redesign yet. If a type really is used only on the main thread, annotate it @MainActor. Leave the architecture changes for later. Where you need an escape hatch (nonisolated(unsafe), @unchecked Sendable, @preconcurrency), add a // TODO(concurrency) comment so you can find every unsafe opt-out later.Flip the module to Swift 6 mode when it has zero concurrency warnings. Run the tests, run them again with Thread Sanitizer (swift test --sanitize=thread), and exercise the app manually to catch the runtime isolation traps described in section 11.Repeat for the next module. When every target is done, set Swift 6 project-wide and work through the TODO list of unsafe opt-outs.

## 5. Fix Global and Static Mutable State

This is usually the largest group of diagnostics, and the most mechanical to fix. Any global or static `var` can be reached from any thread, so Swift 6 rejects it unless it is isolated or protected. (A top-level `var` in a `main.swift` file is implicitly main-actor isolated and does not trigger this.)

**Before** (Swift 6 mode, library code):

```swift
var requestCount = 0

final class Logger {
    static var shared = Logger()
    var lines: [String] = []
}

final class Settings {
    static let shared = Settings()
    var theme = "light"
}
```

The compiler reports:

- `var 'requestCount' is not concurrency-safe because it is nonisolated global shared mutable state`
- `static property 'shared' is not concurrency-safe because it is nonisolated global shared mutable state`
- `static property 'shared' is not concurrency-safe because non-'Sendable' type 'Settings' may have shared mutable state` (for the `let` of a class that is not `Sendable`)

**After:** pick the fix that matches how the state is really used:

```swift
import Foundation

// Immutable and Sendable: nothing to do
let maxRetries = 3

// Only touched from UI code: isolate it to the main actor
@MainActor var requestCount = 0

final class Logger {
    @MainActor static var shared = Logger()
    var lines: [String] = []
}

// A singleton whose state is main-thread-only: isolate the whole type
@MainActor
final class Settings {
    static let shared = Settings()
    var theme = "light"
}

// Already protected by a lock you trust: opt out explicitly and leave a marker
// TODO(concurrency): replace with Mutex once the deployment target allows it
nonisolated(unsafe) var legacyFlags: [String: Bool] = [:]
let legacyFlagsLock = NSLock()
```

Prefer the fixes in this order: make it a `let` of a `Sendable` type, isolate it to a global actor, move it into an actor or behind a `Mutex`, and only as a last resort use `nonisolated(unsafe)`. The last option compiles because you promise the access is synchronized. The compiler no longer checks that promise. These rules come from [SE-0412](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0412-strict-concurrency-for-global-variables.md).

## 6. Make Types Sendable

Swift infers `Sendable` for non-public structs and enums whose stored properties are all `Sendable`. Public types must declare the conformance, and classes need extra rules. When you declare it, the compiler verifies it:

```swift
import Foundation

final class ImageCache {
    var storage: [String: Data] = [:]
}

struct Account: Sendable {
    var owner: String
    var cache: ImageCache
}

final class Counter: Sendable {
    var value = 0
}
```

- `stored property 'cache' of 'Sendable'-conforming struct 'Account' has non-Sendable type 'ImageCache'`
- `stored property 'value' of 'Sendable'-conforming class 'Counter' is mutable`

The fixes, from most to least preferred:

```swift
import Foundation
import Synchronization

// 1. Value types made only of Sendable values
struct Account: Sendable {
    var owner: String
    var balance: Decimal
}

// 2. A final class with only immutable Sendable state
final class APIConfiguration: Sendable {
    let baseURL: URL
    let timeout: Duration
    init(baseURL: URL, timeout: Duration) {
        self.baseURL = baseURL
        self.timeout = timeout
    }
}

// 3. Mutable shared state behind a Mutex (macOS 15 / iOS 18 and later)
final class Counter: Sendable {
    private let value = Mutex(0)

    func increment() -> Int {
        value.withLock { count in
            count += 1
            return count
        }
    }
}

// 4. Mutable state that is naturally accessed asynchronously: an actor
actor ImageStore {
    private var storage: [String: Data] = [:]
    func insert(_ data: Data, for key: String) { storage[key] = data }
}

// 5. Last resort: you guarantee thread safety, the compiler stops checking
// TODO(concurrency): audit; every stored property must be guarded by `lock`
final class LegacyCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Data] = [:]

    func insert(_ data: Data, for key: String) {
        lock.lock()
        defer { lock.unlock() }
        storage[key] = data
    }
}
```

> **Note:** `@unchecked Sendable` does not make a type thread-safe. It only tells the compiler to stop checking. Use it for types that are already synchronized, such as older classes guarded by a lock or a serial queue. If someone later adds a property and forgets the lock, nothing will warn them.

## 7. Fix Main Actor Isolation

Once a view model is `@MainActor`, synchronous callers that are not isolated to the main actor can no longer reach it:

```swift
@MainActor
final class ProfileViewModel {
    var name = ""
    func update(name: String) { self.name = name }
}

func refresh(model: ProfileViewModel) {
    model.update(name: "Ada")
}
```

Error: `call to main actor-isolated instance method 'update(name:)' in a synchronous nonisolated context`. The compiler suggests the two real fixes:

```swift
@MainActor
final class ProfileViewModel {
    var name = ""
    func update(name: String) { self.name = name }
}

// Fix A: the caller is also UI code, so isolate it
@MainActor
func refresh(model: ProfileViewModel) {
    model.update(name: "Ada")
}

// Fix B: the caller runs elsewhere, so make it async and hop with await
func refreshFromBackground(model: ProfileViewModel) async {
    await model.update(name: "Ada")
}
```

A related error comes from `Task.detached`, which drops the caller's isolation:

```swift
@MainActor
final class ViewModel {
    var title = ""
    func load() {
        Task.detached {
            self.title = "Loaded"
        }
    }
}
```

Error: `main actor-isolated property 'title' can not be mutated from a nonisolated context`. This one is an error even in Swift 5 mode. Use a plain `Task`, which inherits the main actor. Move the slow work into a function that runs off the main actor:

```swift
@MainActor
final class ViewModel {
    var title = ""
    func load() {
        Task {
            let fetched = await fetchTitle()   // suspends; the main actor is free meanwhile
            self.title = fetched               // back on the main actor
        }
    }
}

nonisolated func fetchTitle() async -> String {
    "Loaded"
}
```

Use `nonisolated` for members that do not touch isolated state, such as pure helpers or `let` constants of `Sendable` type. Code that is not isolated can then use them without `await`. If a `nonisolated` member reads isolated state, you get `main actor-isolated property 'items' can not be referenced from a nonisolated context`.

## 8. Fix Protocol Conformances That Cross Isolation

A `@MainActor` type cannot satisfy a protocol requirement that is not isolated, such as a delegate protocol, because callers could invoke it from any thread:

```swift
protocol Refreshable {
    func refresh()
}

@MainActor
final class FeedController: Refreshable {
    var items: [String] = []
    func refresh() { items.removeAll() }
}
```

Error: `conformance of 'FeedController' to protocol 'Refreshable' crosses into main actor-isolated code and can cause data races`. Choose one of four fixes:

```swift
protocol DownloadDelegate: AnyObject {
    func downloadDidFinish(bytes: Int)
}

// Option 1 (Swift 6.2+): isolated conformance, usable only on the main actor
@MainActor
final class DownloadsViewModel {
    var total = 0
}

extension DownloadsViewModel: @MainActor DownloadDelegate {
    func downloadDidFinish(bytes: Int) { total += bytes }
}

// Option 2: nonisolated witness for a callback documented to arrive on the main thread
@MainActor
final class ProgressViewModel: DownloadDelegate {
    var total = 0

    nonisolated func downloadDidFinish(bytes: Int) {
        MainActor.assumeIsolated {   // traps if this is not actually the main actor
            total += bytes
        }
    }
}

// Option 3: @preconcurrency conformance: compiles, and checks isolation at run time
@MainActor
final class StatusViewModel {
    var total = 0
}

extension StatusViewModel: @preconcurrency DownloadDelegate {
    func downloadDidFinish(bytes: Int) { total += bytes }
}

// Option 4: if you own the protocol, make the requirement async
protocol AsyncDownloadDelegate: AnyObject {
    func downloadDidFinish(bytes: Int) async
}

@MainActor
final class SummaryViewModel: AsyncDownloadDelegate {
    var total = 0
    func downloadDidFinish(bytes: Int) async { total += bytes }
}
```

The isolated conformance ([SE-0470](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0470-isolated-conformances.md)) is the most precise fix. The compiler then stops you from using the conformance outside the main actor. Assigning the view model to an `any DownloadDelegate` property from `nonisolated` code fails with `main actor-isolated conformance of 'DownloadsViewModel' to 'DownloadDelegate' cannot be used in nonisolated context`.

## 9. Fix Values Sent Between Isolation Domains

Swift 6 uses region-based isolation ([SE-0414](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0414-region-based-isolation.md)). A value that is not `Sendable` can still be passed to an actor or a new task, as long as the compiler can prove that nothing on the sending side uses it afterward. These are the diagnostics you will see when it cannot prove that:

```swift
import Foundation

final class ImageCache {
    var storage: [String: Data] = [:]
}

actor Store {
    var cache = ImageCache()
    func put(_ c: ImageCache) { cache = c }
}

func send(store: Store) async {
    let c = ImageCache()
    await store.put(c)
    c.storage["x"] = Data()       // used after it was handed to the actor
}

func process(cache: ImageCache) async {
    Task.detached {
        cache.storage["a"] = Data()   // cache is still reachable by the caller
    }
}
```

- `sending 'c' risks causing data races`
- `passing closure as a 'sending' parameter risks causing data races between code in the current isolation context and concurrent execution of the closure`

Fix it by making the handoff the value's last use, by marking parameters and results `sending` ([SE-0430](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0430-transferring-parameters-and-results.md)), or by making the type `Sendable`:

```swift
import Foundation

final class ImageCache {
    var storage: [String: Data] = [:]
}

actor Store {
    var cache = ImageCache()
    func put(_ c: sending ImageCache) { cache = c }
}

// The result is a fresh value the caller fully owns
func makeCache() -> sending ImageCache {
    let c = ImageCache()
    c.storage["seed"] = Data()
    return c
}

func fill(store: Store) async {
    let c = makeCache()
    c.storage["x"] = Data()   // mutate first...
    await store.put(c)        // ...then hand it off as the last use
}
```

## 10. Handle Modules You Don't Control

When a dependency has not adopted concurrency annotations, its classes look non-`Sendable` to your code. Swift 6.4 points you to the import:

```swift
import LegacyKit

struct Session: Sendable {
    let token: Token   // Token is a class from LegacyKit
}
```

You get `stored property 'token' of 'Sendable'-conforming struct 'Session' has non-Sendable type 'Token'`, plus `add '@preconcurrency' to treat 'Sendable'-related errors from module 'LegacyKit' as warnings` on the import line. The fix:

```swift
// TODO(concurrency): drop @preconcurrency once LegacyKit ships Sendable annotations
@preconcurrency import LegacyKit
```

Treat this as temporary. In a test against a module built without concurrency checking, `@preconcurrency` removed the `Sendable` diagnostics for that module's types completely. It did not just turn them into warnings. A `Sendable` struct that stores one of those classes then compiles silently. Before you add the attribute, check whether the type really is safe to share. If it is not, wrap it in your own `Sendable` type, such as an actor, or copy out the `Sendable` values you need. The rules come from [SE-0337](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0337-support-incremental-migration-to-concurrency-checking.md).

## 11. Callback APIs and Runtime Isolation Traps

This pitfall gets past the compiler. In Swift 6 mode, synchronous closures isolated to the main actor that you pass to a module compiled without strict concurrency checking get a runtime isolation check ([SE-0423](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0423-dynamic-actor-isolation.md)). Here `fetchToken` comes from a Swift 5 module and calls its completion handler on a global dispatch queue:

```swift
import LegacyKit

@MainActor
final class SessionModel {
    var tokenValue = ""

    func start() {
        fetchToken { token in             // inferred @MainActor, but called on a background queue
            self.tokenValue = token.value
        }
    }
}
```

It compiles without warnings in both modes. Built in Swift 5 mode, it runs and silently mutates main-actor state on a background thread. Built in Swift 6 mode, the process stops with `EXC_BREAKPOINT` (`SIGTRAP`) the moment the closure runs. The backtrace shows `_dispatch_assert_queue_fail`, reached from the concurrency runtime's executor check (`swift_task_isCurrentExecutorWithFlagsImpl`). That is the data race finally being reported. Fix it at the boundary:

```swift
@preconcurrency import LegacyKit

// Option A: say the callback can run anywhere, then hop to the main actor
@MainActor
final class SessionModel {
    var tokenValue = ""

    func start() {
        fetchToken { @Sendable token in
            let value = token.value               // copy out a Sendable value
            Task { @MainActor in
                self.tokenValue = value
            }
        }
    }
}

// Option B (better): wrap the callback API once, then use async/await everywhere
func fetchTokenValue() async -> String {
    await withCheckedContinuation { continuation in
        fetchToken { token in
            continuation.resume(returning: token.value)
        }
    }
}

@MainActor
final class SessionModel2 {
    var tokenValue = ""

    func start() async {
        tokenValue = await fetchTokenValue()
    }
}
```

Search the codebase for completion handlers, `NotificationCenter` observers, KVO, timers, and delegate callbacks from C or Objective-C libraries. Any of them can deliver a call on a thread you did not expect. Exercise those code paths before you ship a Swift 6 build.

## 12. Mapping Table: Swift 5 Pattern to Swift 6 Replacement

| Swift 5 pattern | Swift 6 replacement |
| --- | --- |
| `static var shared = X()` | `static let shared` with a `Sendable` or `@MainActor` type |
| Global `var` used from UI code | `@MainActor var` |
| Global `var` guarded by a lock | `Mutex`, or `nonisolated(unsafe) var` with a TODO |
| Class with a private serial queue | `actor`, or `final class: Sendable` with `Mutex` |
| `DispatchQueue.main.async { … }` | `@MainActor` function, or `Task { @MainActor in … }` |
| `Task.detached` to "get off the main thread" | `Task { }` calling a `nonisolated` or `@concurrent` async function |
| Delegate conformance on a UI class | `extension T: @MainActor Delegate` (Swift 6.2+) |
| Completion-handler API | `withCheckedContinuation` or `withCheckedThrowingContinuation` wrapper |
| Class "known" to be thread-safe | `final class: Sendable` with immutable state, or `@unchecked Sendable` after an audit |
| Import of an older dependency | `@preconcurrency import` (temporary) |
| Passing a fresh non-`Sendable` object to an actor | `sending` parameter, with the handoff as the last use |

## 13. Common Errors and Their Fixes

Messages are from Swift 6.4. Recent compilers add a bracketed diagnostic group such as `[#MutableGlobalVariable]` that links to an explanation on docs.swift.org. In Swift 5 mode with complete checking, the same text appears as a warning ending in "; this is an error in the Swift 6 language mode".

| Message (abridged) | Usual fix |
| --- | --- |
| `var 'x' is not concurrency-safe because it is nonisolated global shared mutable state` | `let`, `@MainActor`, `Mutex`, or `nonisolated(unsafe)` (section 5) |
| `static property 'shared' is not concurrency-safe because non-'Sendable' type 'X' may have shared mutable state` | Make `X` `Sendable` or `@MainActor` |
| `stored property 'p' of 'Sendable'-conforming class 'X' is mutable` | `let`, `Mutex`, or an actor |
| `capture of 'x' with non-Sendable type 'X' in a '@Sendable' closure` | Capture a `Sendable` copy, or make `X` `Sendable` |
| `call to main actor-isolated instance method 'm()' in a synchronous nonisolated context` | `@MainActor` on the caller, or make it `async` and `await` |
| `main actor-isolated property 'p' can not be mutated from a nonisolated context` | Use `Task { }` instead of `Task.detached`, or call an isolated method |
| `conformance of 'X' to protocol 'P' crosses into main actor-isolated code and can cause data races` | Isolated conformance, `nonisolated` witness, or `@preconcurrency` (section 8) |
| `sending 'x' risks causing data races` | Don't use `x` after the handoff, use `sending`, or make it `Sendable` |
| `passing closure as a 'sending' parameter risks causing data races …` | Don't capture state the caller still uses. Copy values into the closure. |
| `add '@preconcurrency' to treat 'Sendable'-related errors from module 'M' as warnings` | `@preconcurrency import M` after checking the types (section 10) |

## 14. Swift 6.2+ Options: Default MainActor Isolation and Approachable Concurrency

Swift 6.2 added two opt-in settings that change the defaults instead of the rules:

- **Default actor isolation** ([SE-0466](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0466-control-default-actor-isolation.md)): `SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`, `.defaultIsolation(MainActor.self)` in a `swift-tools-version: 6.2` manifest, or `-default-isolation MainActor`. Every declaration without an explicit annotation is inferred `@MainActor`, so the global-state and UI errors above disappear for code that really is single-threaded. This works well for app targets. It is usually the wrong choice for libraries whose types are used from many threads.
- **Approachable Concurrency** (`SWIFT_APPROACHABLE_CONCURRENCY = YES`): according to Xcode's own description of the setting, it turns on `DisableOutwardActorInference`, `GlobalActorIsolatedTypesUsability`, `InferIsolatedConformances`, `InferSendableFromCaptures` and `NonisolatedNonsendingByDefault`. The last one ([SE-0461](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md)) makes `nonisolated async` functions run on the caller's actor unless they are marked `@concurrent`.

Under default `MainActor` isolation, opt value types and background work out explicitly:

```swift
import Foundation

// Compiled with -default-isolation MainActor
nonisolated struct Point: Sendable {
    var x = 0, y = 0
    func moved() -> Point { Point(x: x + 1, y: y) }
}

struct Report: Sendable {
    var lines: [String]
}

@concurrent
func parseReport(_ data: Data) async -> Report {
    Report(lines: String(decoding: data, as: UTF8.self)
        .split(separator: "\n").map(String.init))
}
```

Without the `nonisolated`, `Point` becomes main-actor isolated. Calling `moved()` from `Task.detached` then fails with `main actor-isolated instance method 'moved()' cannot be called from outside of the actor`. If you enable `NonisolatedNonsendingByDefault` on existing code, `swift package migrate --to-feature NonisolatedNonsendingByDefault` (the `migrate` command arrived in Swift 6.2, [SE-0486](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0486-adoption-tooling-for-swift-features.md)) adds `@concurrent` where needed to keep today's behavior and updates the manifest for you.

## 15. Pitfalls

- **Annotating everything `@MainActor`.** It silences errors but moves work onto the main thread. Keep parsing, image decoding and disk I/O in `nonisolated`/`@concurrent` functions or actors.
- **Using `@unchecked Sendable` and `nonisolated(unsafe)` as defaults.** They are promises the compiler cannot check. Track each one with a TODO and remove it later.
- **Actor reentrancy.** State read before an `await` inside an actor may have changed after it. Re-check your assumptions after every suspension point, or keep critical sections synchronous.
- **Leaving `@preconcurrency` in place forever.** It can hide real races in dependency types (section 10). Remove it when the dependency updates.
- **Testing only the compile.** Runtime isolation traps (section 11) appear only when the code path runs. Run the full test suite and the app in Swift 6 mode.
- **Treating `Task.detached` as "background".** It also drops priority inheritance and task-local values. A `nonisolated`/`@concurrent` async function called from a regular `Task` is usually what you want.
- **Forgetting test targets.** Test targets are modules too, with their own language mode. Mark UI-facing tests `@MainActor`.

## 16. Verification Checklist

- Every target's language mode is set on purpose and shows in the manifest or xcconfig.
- Modules still in Swift 5 mode have complete checking on and zero concurrency warnings, or a tracked list of what is left.
- `git grep -n -E "nonisolated\(unsafe\)|@unchecked Sendable|@preconcurrency"` lists only opt-outs that each have a TODO and a reason.
- Unit and UI tests pass in Swift 6 mode, and `swift test --sanitize=thread` (or the Thread Sanitizer scheme option) reports nothing.
- You have manually exercised the callback paths (completion handlers, delegates, notifications, timers) in a Swift 6 build.
- CI builds with the same Xcode version you use locally.

## 17. Key Points

- The language mode is set per module. You migrate one module at a time without raising the deployment target.
- Swift 5 mode with complete checking reports the Swift 6 data-race errors as warnings, so you can fix issues while the build keeps working. A few isolation violations are errors in both modes.
- Fix global state first, then `Sendable` types, isolation, conformances, sending, and dependencies.
- Prefer real fixes (`let`, `Sendable` value types, actors, `Mutex`, `@MainActor`) over opt-outs (`@unchecked Sendable`, `nonisolated(unsafe)`, `@preconcurrency`).
- Swift 6 mode adds runtime isolation checks. Callbacks from code that was not checked can trap where Swift 5 silently raced.
- Swift 6.2 adds isolated conformances, default `MainActor` isolation and `@concurrent`, which make app targets much easier to migrate.

## 18. Final Summary

Moving to the Swift 6 language mode means proving to the compiler that your shared mutable state is safe. Do it module by module. Turn on complete checking in Swift 5 mode, work through the warnings category by category, and switch the module to Swift 6 only when it is warning-free. Most diagnostics have a small, local fix: a `let`, a `Sendable` conformance, a `@MainActor` annotation, an isolated conformance, or a `sending` parameter.

Use the escape hatches deliberately and keep track of them. Expect the first Swift 6 builds to show runtime isolation traps in callback-heavy code. Those traps are real data races that were already in the app. For deeper background, read the official [Swift 6 migration guide](https://www.swift.org/migration/documentation/migrationguide/) and its [common compiler errors](https://www.swift.org/migration/documentation/swift-6-concurrency-migration-guide/commonproblems) chapter.
