Home / Blog / Swift 6 concurrency migration: are you actually ready?

Swift 6 concurrency migration: are you actually ready?

Swift 6 makes strict concurrency checking the default. How I migrate a 100K+ LOC codebase, and how to really understand actors, Sendable, and isolation.

Swift 6 is here and the biggest change is strict concurrency checking. The compiler now catches thread safety problems at compile time. Once Xcode 16 makes Swift 6 mode the default, older codebases are going to face a wall of warnings and errors.

I migrated two of my iOS apps to Swift 6, roughly 60K LOC total. The process was not easy, but I learned plenty. Here is the migration approach I ended up using.

What Swift 6 actually changes

The biggest shifts:

  1. Compile-time data race detection. Are two threads touching the same data? The compiler warns you.
  2. Sendable enforcement. Data crossing actor boundaries must be Sendable. That guarantees it is thread safe.
  3. Strict actor isolation. @MainActor and @globalActor annotations are now mandatory.
  4. Complete concurrency checking. The -strict-concurrency flag from Swift 5.5 is now the default.

Net result: you can no longer ship concurrent code that “seems to work”. The compiler questions it.

Pre-migration: state assessment

Before you start migrating, assess where you are.

1. Current Swift version: on 5.9+ the migration is easier. Below 5.5, upgrade to an intermediate version first.

2. Async/await adoption: how much of the project uses async/await? A lot, easy migration. Callback heavy codebase, harder road.

3. Third party SDKs: are they all Swift 6 ready? Some still are not. Build compatibility risk.

4. Test coverage: migration is refactoring. Without tests the regression risk is very high.

Estimate the migration length from those four factors. For Dentii:

Swift 5.9. Async/await 70% adopted. 90% of SDKs ready. Test coverage 60%.

Estimate: 2 to 3 weeks. Actual: 4 weeks (it always takes longer).

Migration phases

Phase 1: warning mode

Xcode project settings, Swift Compiler > Upcoming Features, enable:

BareSlashRegexLiterals. ConciseMagicFile. ImplicitOpenExistentials. DisableOutwardActorInference. StrictConcurrency (the critical one).

With that flag the Swift 6 behaviour shows up as warnings while the build still passes. You can see the issues in the codebase.

Dentii’s first build: 1247 warnings.

Phase 2: fix in batches

You cannot fix 1247 warnings in one go. Group them by category:

  • Sendable conformance issues (about 400)
  • Main actor isolation (about 300)
  • Data race in closures (about 200)
  • Actor boundary crossing (about 150)
  • Other (about 200)

A separate commit and PR per category. Fully fix one category, merge, move to the next.

Phase 3: enable Swift 6 mode

Last phase: Swift Language Mode = Swift 6. Warnings flip to errors. The build has to be green.

If the earlier phases are clean this is straightforward. If not, expect 2 to 3 more days of work.

Sendable conformance

The most common issue. A type sent across actor boundaries has to be Sendable.

Problem:

class User {
    var name: String
    var email: String
}

// Warning: Non-sendable type cannot be sent across actors
Task {
    let user = await fetchUser()
    await mainActor.updateUI(user)
}

Fix options:

1. Switch to a struct (if possible):

struct User: Sendable {
    let name: String
    let email: String
}

2. Final class + Sendable:

final class User: Sendable {
    let name: String  // immutable
    let email: String
}

3. Define it as an actor:

actor User {
    var name: String
    var email: String
}

Which fix depends on context:

Data transfer object: struct. Reference semantics required: final class plus immutable. Mutating state: actor.

MainActor isolation

UI code has to run on a single actor, MainActor. The Swift 6 compiler enforces that.

Problem:

class ViewController: UIViewController {
    var data: [Item] = []
    
    func loadData() {
        Task {
            let items = try await api.fetchItems()
            self.data = items  // Warning: UI update off-main-thread
        }
    }
}

Fix: MainActor annotation:

@MainActor
class ViewController: UIViewController {
    var data: [Item] = []
    
    func loadData() {
        Task {
            let items = try await api.fetchItems()
            self.data = items  // OK, class is MainActor-isolated
        }
    }
}

UIKit and SwiftUI view controllers and views have to be MainActor. Swift 6 assumes that out of the box, but legacy code often has it missing.

Data race in closures

The trickiest category. An escaping closure capturing mutable state.

Problem:

var counter = 0
DispatchQueue.global().async {
    counter += 1  // Warning: data race
}

Fix options:

1. Move it into an actor:

actor Counter {
    var value = 0
    func increment() { value += 1 }
}
let counter = Counter()
Task { await counter.increment() }

2. @MainActor:

@MainActor var counter = 0
Task { @MainActor in
    counter += 1
}

3. Immutable + computed:

If you can, make the state immutable and expose it as a computed property.

Actor boundary crossing

A method on actor A passing a parameter to actor B.

Problem:

actor DatabaseActor {
    func save(_ user: User) async { /* ... */ }
}

actor NetworkActor {
    func fetchUser() async -> User { /* ... */ }
}

// Called from MainActor
let user = await network.fetchUser()
await database.save(user)  // User has to be Sendable

If User is not Sendable, the compiler complains. Fix: make User Sendable.

Third party SDK issues

An SDK not being Swift 6 ready is a common roadblock.

Strategy:

1. Update to the latest version. Most popular SDKs (Firebase, Alamofire, RxSwift) are ready now.

2. The -enable-experimental-feature NonSendableObjC flag. Loosens the sendable check on the Objective-C bridge.

3. Write a wrapper class. Wrap the SDK API in your own Sendable wrapper.

4. Sometimes @preconcurrency import. Import the library in legacy mode.

An older analytics SDK caused trouble in Dentii. I worked around it with @preconcurrency import, then pulled it out once the SDK was updated.

Testing during migration

Run the test suite continuously through the migration. Catch regressions early.

Concurrency-specific tests:

Thread Sanitizer (TSan) turned on in Xcode. Load tests (many concurrent operations). Race condition scenarios.

Those catch runtime issues that the compiler cannot catch.

Performance impact

Does performance shift with Swift 6 concurrency?

Usually no. These are compile-time checks. Runtime overhead is minimal.

Actors use a message passing pattern. Slightly slower than a function call, but the difference is below 1ms.

Benchmark after migration. If there is a regression, profile specific actors.

Benefits after migration

Once you are through the pain, what you gain:

  1. Data races are impossible. The compiler proves it.
  2. Clearer code structure. Actors draw explicit state boundaries.
  3. Better async code. Concurrency bugs drop.
  4. Future proof. Apple’s concurrency tooling will build on top of Swift 6.

Short term pain, long term gain.

Should I migrate now?

Swift 6 language mode is opt-in right now. From Xcode 16 onward it becomes the default.

Migration recommendation:

New projects: Swift 6 default, write the codebase Sendable-conscious from day one. Mature projects (5+ years): phased migration over 2 to 3 release cycles. Small codebase (under 20K LOC): aggressive migration, a week of focused work. Large codebase (100K+ LOC): 1 to 2 months of dedicated effort.

If you are not ready, turn off the Xcode 16 default and push it out. But within 1 to 2 years it will be mandatory.

Takeaway

Swift 6 concurrency migration is inevitable. Apple is signalling the direction clearly. The longer you delay, the more painful it gets.

Phased approach: warning mode first, fix in batches, flip to Swift 6 mode last. 2 to 4 weeks for a medium codebase.

Sendable, @MainActor, actors are the core concepts. Do not start the migration without understanding them. Once the mental model clicks, the fixes become mechanical.

End result: data race free, more maintainable code. The investment pays off.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch