Surviving Swift 6's Sendable Warnings
Strict concurrency checking turned a quiet codebase into a wall of yellow. Here is the order we fixed things in.
Turning on the Swift 6 language mode in a mid-sized app produced just over three hundred concurrency diagnostics. Most were variations on a handful of themes, and fixing them in the right order made the rest collapse quickly.
The key idea is that Swift 6 wants proof that data crossing an isolation boundary is safe. A type is safe to share if it is Sendable: value types made of sendable parts, actors, and classes that are immutable or protected by the compiler-visible isolation of an actor.
1. Pin the UI layer to the main actor
Most warnings in an app target come from view models and controllers touched from both the UI and async work. Annotating them with @MainActor resolves the majority in one stroke, because everything inside now shares one isolation domain:
1
2
3
4
5
6
7
8
9
10
11
12
@MainActor
final class FeedViewModel {
private(set) var items: [Item] = []
private let api: FeedAPI
init(api: FeedAPI) { self.api = api }
func refresh() async throws {
// Hops off the main actor inside `fetch`, back on for the assignment.
items = try await api.fetch()
}
}
2. Make models value types
Plain data models that are structs of strings, numbers and dates are inferred Sendable automatically when they’re not public. For public types, add the conformance explicitly. If a model is a class only out of habit, make it a struct.
3. Wrap shared mutable state in an actor
Caches and in-memory stores that are touched from many tasks belong in an actor. Callers await their way in, and the compiler stops worrying.
4. Use @unchecked Sendable last, and document it
Some types are thread-safe through a lock the compiler can’t see. Marking them @unchecked Sendable is legitimate, but leave a comment explaining the invariant. Every one of these is a promise the compiler is no longer checking for you.
Migrate module by module with the per-target Swift language version setting, and keep the warnings as warnings until a target is clean.
— 30 —