How would you migrate a large codebase to Swift 6 without a freeze?
Predict first
What does strict concurrency checking give you in mode 5?
struct Counter {
static var count = 0
}
Counter.count += 1
- Errors that break the build
- Warnings that preview mode 6
Predict first
Which is the safest first step?
// A: set every module to Swift 6 at once
// B: enable complete strict concurrency as warnings in mode 5 first
// C: add @unchecked Sendable everywhere
- A
- B
- C
Mental model. Repainting a bridge one lane at a time. Traffic keeps flowing, and each finished lane stays finished.
Move in stages that each ship. First update the toolchain and your dependencies while every module stays in mode 5. Then switch on strict concurrency checking in warning form (-strict-concurrency=complete in mode 5), which shows what mode 6 would reject without breaking the build. Fix the warnings by category: shared mutable state becomes an actor, is isolated to @MainActor, or is marked nonisolated(unsafe) with a written justification; values that cross domains become Sendable; callbacks become @Sendable; and old dependencies are imported with @preconcurrency until they catch up.
Go module by module, from the leaves (modules with no dependents) upwards, setting each module’s mode to 6 once it is clean and keeping the rest in mode 5. Resist @unchecked Sendable as a shortcut: it silences the compiler without making the code safe. Add CI jobs that build clean modules in mode 6 so they cannot regress, and adopt other upcoming features separately. The work is mostly design (who owns this state?), not syntax, so involve the owners of each subsystem.
In mode 5, -strict-concurrency=complete reports the problems as warnings, so you can fix them without breaking the build.
It shows the work without breaking anything, and lets you fix it in small, shippable pieces.
Common traps
- Flipping the whole project to mode 6 at once. You get thousands of errors and a long-lived branch.
- Sprinkling
@unchecked Sendableto make the build green. The races are still there, only hidden.
They might follow up with
- What is
@preconcurrency importfor? It downgrades the strictness for a dependency you cannot change yet. - How do you prioritise? Start with leaf modules and shared state with obvious owners, and leave the app target for last.
Related questions
- 5.7.1 What is the difference between the Swift 6 compiler and Swift 6 language mode?
- 5.7.2 What are upcoming features and experimental features, and how do they help a migration?
- 1.10.2 What does Sendable mean for a Swift type you write?
← What are upcoming features and experimental features, and how do they…