What changed with strict concurrency in Swift 6, and how do you migrate?
TL;DR
In the Swift 6 language mode, the compiler proves your code is free of data races. Possible races are errors, not warnings.
Every piece of mutable state must be protected: by an actor (like @MainActor), or by being Sendable.
Migrate step by step: turn on complete checking as warnings in Swift 5 mode, fix them module by module, then switch to Swift 6 mode.
What is a data race?
Two threads access the same memory at the same time, and at least one writes. The result is unpredictable. Crashes are random and hard to reproduce.
var counter = 0 // global mutable state
func increment() { counter += 1 }
// Swift 6 error: var 'counter' is not concurrency-safe because it is
// nonisolated global shared mutable stateThe Swift 6 compiler vs the Swift 6 language mode
- The Swift 6 compiler can still build code in Swift 5 mode. Your old code keeps working.
- The Swift 6 language mode turns on full data-race safety as errors.
You choose the mode per target. In Xcode: Build Settings, "Swift Language Version". In a package: swiftLanguageModes: [.v6] (or per target).
The main rules
1. Values that cross isolation must be Sendable
final class Settings { // not Sendable: mutable class
var theme = "dark"
}
actor Store {
func save(_ s: Settings) { }
}
func run(store: Store) async {
let s = Settings()
await store.save(s)
s.theme = "light" // error: s was sent to the actor and then used again
}Fixes: make Settings a struct, make it an actor, or make it a final class with only let properties.
2. Global and static variables must be safe
@MainActor var currentUser: User? // isolated to the main actor
let apiBase = URL(string: "https://api.example.com")! // immutable + Sendable
nonisolated(unsafe) var legacyFlag = false // "trust me", use rarely3. UI code must run on the main actor
UIKit and SwiftUI types are @MainActor. Code that touches them must be on the main actor too.
@MainActor
final class ProfileViewModel {
var name = ""
let api = APIClient() // an actor, or a Sendable type
func load() async {
let user = try? await api.fetchUser() // the request runs off the main actor
name = user?.name ?? "" // back on the main actor here
}
}Region-based isolation and sending
Swift 6 is smarter than the Swift 5.10 checks. It tracks regions of values. A non-Sendable value can still move to another actor if you never use it again after. In the Settings example above, await store.save(s) is fine on its own. Only the use after it is an error.
The sending keyword marks parameters and results that transfer ownership like this.
Swift 6.2: approachable concurrency
Swift 6.2 makes the defaults easier for app code. New Xcode 26 projects turn these on:
- Default main actor isolation. Code in the module is
@MainActorunless you say otherwise. Great for app targets, where most code is UI. nonisolated asyncfunctions run on the caller's actor (nonisolated(nonsending)by default). They no longer jump to a background thread automatically.@concurrentmarks a function that should run on the background pool.
// With default MainActor isolation on:
final class ThumbnailMaker { // implicitly @MainActor
@concurrent
nonisolated func makeThumbnail(from data: Data) async -> Data {
resize(data) // heavy work, runs off the main actor
}
}How to migrate
- Update dependencies first.
- In Swift 5 mode, set Strict Concurrency Checking = Complete. You get warnings, not errors.
- Fix one module at a time. Start with leaf modules (models, utilities).
- Mark UI types
@MainActor. Make models value types orSendable. - Use
@preconcurrency import SomeOldSDKfor modules that are not annotated yet. - Switch the module to Swift 6 mode when it has no warnings.
Escape hatches (use with care)
| Tool | What it does |
|---|---|
@unchecked Sendable | You promise the type is safe (for example, it uses a lock) |
nonisolated(unsafe) | Turns off checking for one variable |
@preconcurrency | Softens errors from old APIs or conformances |
MainActor.assumeIsolated { } | Asserts at run time that you are on the main actor |
Each one moves the safety check from the compiler to you. Add a comment saying why it is safe.
Common follow-up questions
- Does Swift 6 mode make my app faster? No. It makes it safer. Runtime behavior is the same unless you change isolation.
- Can I mix Swift 5 and Swift 6 modules? Yes. The mode is per module, so you can migrate gradually.
- Why do I get errors in delegate callbacks? Old delegate protocols are not annotated. The compiler does not know which thread calls them. Use
@preconcurrencyconformance or mark the methodnonisolatedand hop to the main actor. - Swift 5.10 vs Swift 6 checks? Swift 5.10 showed them as warnings. Swift 6 adds region-based isolation (fewer false errors) and makes them errors.
- Is
@unchecked Sendablea bad smell? Not always. It is fine for a class that really protects its state with a lock. It is bad when used only to silence the compiler.