Swift 6 marks a turning point in the evolution of Apple’s language. It brings stricter concurrency rules, typed error handling, new ownership models, and refined import controls — all designed to make your code safer, faster, and more maintainable.
But with great features comes… great compiler errors. Migrating an existing app to Swift 6 requires strategy, patience, and some caffeine. This handbook provides a step-by-step migration plan, code examples for every major change, and a pitfalls section to help you avoid common traps.
Swift 6 enforces data race safety by default. This is the single biggest migration hurdle.
Swift 5.9 (compiles, but unsafe):
class Counter {
var value = 0
}
let counter = Counter()
DispatchQueue.global().async {
counter.value += 1 // ⚠️ Potential data race
}
Swift 6 (actor-based safety):
actor Counter {
private var value = 0
func increment() {
value += 1
}
func getValue() -> Int {
value
}
}
let counter = Counter()
Task {
await counter.increment()
}
Why this matters: Actors guarantee mutual exclusion for state, eliminating data races without forcing you to juggle locks.
Typed errors make exception handling explicit and safer.
enum ValidationError: Error {
case emptyName
case invalidEmail
}
func validate(name: String) throws(ValidationError) {
guard !name.isEmpty else { throw .emptyName }
}
Benefits:
Granular visibility on imports keeps modules clean.
internal import AnalyticsFramework
private import InternalUtils
No more leaking private frameworks into your public API surface.
~Copyable)Swift 6 introduces move-only types for memory safety.
struct FileHandle: ~Copyable {
let descriptor: Int
}
// Move semantics: ownership transfers
let handle = FileHandle(descriptor: 10)
let newHandle = handle // handle is no longer valid
Why it matters: Prevents accidental sharing of low-level resources (like file descriptors) that must have unique ownership.
Even before flipping to Swift 6, enable strict concurrency in Swift 5.x.
In Package.swift:
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
In Xcode:
Build Settings → Swift Compiler - Custom Flags → -Xfrontend -enable-actor-data-race-checks
This surfaces issues early so you can fix them incrementally.
Start with small refactors.
Before:
func fetchData(completion: @escaping () -> Void) {
DispatchQueue.global().async {
completion()
}
}
After:
func fetchData(completion: @Sendable @escaping () -> Void) {
Task {
await completion()
}
}
UI updates:
@MainActor
func updateUI() {
// Safe main-thread UI code
}
Don’t flip the whole app at once. In Xcode:
Build Settings → Swift Language Version → Swift 6
Start with utility modules, then core business logic, then the app target.
Async/await is central to Swift 6. Update your tests accordingly.
func testLogin() async throws {
let result = try await loginManager.login(username: "user", password: "pass")
XCTAssertTrue(result.success)
}
Unmarked Closures
Forgetting @Sendable will cause compile errors.
let task = Task {
await doWork() // closure must be @Sendable
}
UI Updates Without @MainActor
Runtime crashes await those who forget.
Shared Mutable State Refactor into actors. Don’t patch with locks.
Third-Party Dependencies Some libraries may lag behind Swift 6. Consider forking or replacing.
Typed Throws Misuse
Don’t declare throws(Error) everywhere. Be specific.
Non-Copyable Confusion
Remember: ~Copyable means values can’t be duplicated. Treat them like unique tokens.
| Task | Before Migration (Swift 5.x) | After Migration (Swift 6) |
|---|---|---|
| Concurrency enforcement | Warnings only | Compile-time errors |
| Shared mutable state | Classes with locks | Actors preferred |
| Error handling | throws (any error) |
Typed throws(MyError) |
| Module imports | All public | Controlled with internal/private |
| Ownership model | Copyable by default | ~Copyable support |
| Test style | Completion handlers | Async/await tests |
Q: My closure stopped compiling. Why?
A: It probably needs @Sendable. The compiler is stricter in Swift 6.
Q: My app crashes updating UI in async tasks.
A: Mark UI code with @MainActor. Swift 6 enforces main-thread UI access.
Q: Do I need to migrate all at once? A: No. Swift 6 supports per-target language settings. Migrate gradually.
Q: Should I replace all classes with actors? A: No. Use actors for shared mutable state, not everything. Value types and isolated classes are still valid.
Recommended reading:
👉 Migrating to Swift 6 isn’t just about “making it compile.” It’s about adopting modern Swift practices that will pay dividends in stability, performance, and maintainability. Treat migration as an investment, not a chore.