---
title: "Swift 6 strict concurrency: a calm, step-by-step migration"
description: "Swift 6 turns data races into compile errors. Here's a step-by-step way to move an existing iOS app without drowning in warnings, plus what Swift 6.2 changed."
author: "MH TOUFIK"
published: 2026-10-30T15:23:37.435Z
updated: 2026-10-30T19:23:37.000Z
url: https://www.toufikforyou.dev/blog/swift-6-strict-concurrency-migration-guide
category: "iOS"
tags: ["Swift", "Swift 6", "Swift Concurrency", "SwiftUI", "iOS"]
---

# Swift 6 strict concurrency: a calm, step-by-step migration

The first time you switch an existing iOS project to the Swift 6 language mode, it's a shock. A project that built cleanly yesterday suddenly shows a wall of errors about `Sendable`, actors, and "data races."

It looks bad, but it's fixable — and the errors point at real bugs. Here's a calm, step-by-step way through it, plus what Swift 6.2 changed to make it easier.

## What Swift 6 actually changed

A **data race** happens when two threads touch the same data at the same time and at least one of them writes to it. These bugs are nasty: random crashes, corrupted state, and failures you can never reproduce on your own phone.

Swift 6 makes data-race safety part of the compiler. In the Swift 6 language mode, code that *could* race isn't a warning anymore — it's an error. The compiler now proves your concurrent code is safe before it lets you ship it.

That's the whole idea. Every error is the compiler saying: "I can't prove this is safe."

## Migrate in steps, not all at once

The worst way to migrate is to flip a large project straight to Swift 6 mode and stare at 400 errors. Apple's own guidance is to climb gradually, using the **Strict Concurrency Checking** build setting in Swift 5 mode first.

![The step-by-step path to the Swift 6 language mode](https://www.toufikforyou.dev/media/swift-migration-ladder-2e7b4d93.webp "Turn on stricter checking in stages, fix warnings at each step, and switch the language mode last — one module at a time.")

1. **Minimal** — the default. `Sendable` is only enforced where you've explicitly adopted it.
2. **Targeted** — checks everywhere your code has adopted concurrency (`async`/`await`, actors, `Sendable`). Fix these warnings first.
3. **Complete** — checks the whole module, exactly like Swift 6 mode, but still as warnings.
4. **Swift 6 language mode** — once the warnings are gone, switch the module over and they become errors that keep you safe from now on.

Do it **one module at a time**, starting with the small ones at the bottom of your dependency graph. Each module you finish stays finished.

## The fixes you'll use most

Most warnings fall into a few patterns.

**1. UI code belongs on the main actor.** View models and anything that touches UI should say so:

```swift
@MainActor
final class LibraryViewModel: ObservableObject {
    @Published var books: [Book] = []

    func load() async {
        books = await repository.fetchBooks()
    }
}
```

**2. Prefer value types for data you pass around.** A `struct` whose properties are all `Sendable` is automatically `Sendable` when it's not public — so models can cross threads safely without any extra work:

```swift
struct Book: Identifiable, Hashable {
    let id: UUID
    let title: String
    let author: String
}
```

**3. Put shared mutable state in an actor.** If several tasks really do need to change the same data, an actor makes access one-at-a-time by design:

```swift
actor ImageCache {
    private var images: [URL: UIImage] = [:]

    func image(for url: URL) -> UIImage? { images[url] }
    func store(_ image: UIImage, for url: URL) { images[url] = image }
}
```

**4. Old dependencies:** if a library you depend on hasn't been updated for Swift 6 yet, `@preconcurrency import` quiets its warnings while you wait for an update. Use it as a temporary bridge, not a permanent fix.

## What Swift 6.2 made easier

Swift 6.2 (released in September 2025) introduced what the Swift team calls **approachable concurrency**. Two changes matter most for app developers:

- **Main actor by default.** You can choose to isolate a whole module to the main actor, without writing `@MainActor` everywhere. For app targets — where most code runs on the main thread anyway — this removes a huge amount of annotation noise.
- **Async functions stay where they're called.** With the new upcoming feature enabled, a `nonisolated` async function runs in the caller's context instead of jumping to a background thread. When you really *do* want work off the main actor, you mark it explicitly with `@concurrent`.

```swift
// Explicitly runs on the concurrent thread pool, off the main actor
@concurrent
func decodeLargeFeed(_ data: Data) async throws -> [Book] {
    try JSONDecoder().decode([Book].self, from: data)
}
```

The result: concurrency becomes opt-in where it matters, instead of a puzzle you have to solve on every line.

## Why I'm glad this exists

I'm building the iOS version of [Books My Friend](https://github.com/toufikforyou/books-my-friend-iOS) in Swift, after first building it for Android in Kotlin. Loading book data, caching images, and updating the UI from async work is exactly where race bugs hide.

Having the compiler point at those spots *before* a user hits them is worth the extra effort up front.

## A quick migration checklist

1. Update to the latest Xcode and update your dependencies.
2. Set Strict Concurrency Checking to **Targeted** and fix the warnings.
3. Move UI types and view models to `@MainActor`.
4. Make your models `struct`s with `Sendable` properties.
5. Move shared mutable state into actors.
6. Raise checking to **Complete** and finish the remaining warnings.
7. Switch to the Swift 6 language mode — one module at a time.

Take it slowly, module by module. The compiler is on your side here.

## Sources

- [Announcing Swift 6](https://www.swift.org/blog/announcing-swift-6/) — Swift.org
- [Swift 6.2 Released](https://www.swift.org/blog/swift-6.2-released/) — Swift.org
- [Migrating to Swift 6](https://www.swift.org/migration/documentation/migrationguide/) — Swift.org
- [Migrate your app to Swift 6 (WWDC24)](https://developer.apple.com/videos/play/wwdc2024/10169/) — Apple Developer
- [SE-0461: Run nonisolated async functions on the caller's actor by default](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0461-async-function-isolation.md) — Swift Evolution

