Preparing for Swift 6 Strict Concurrency

Swift 6 enforces strict concurrency checking by default. Here's how to prepare your codebase now.
Follow along with the code: iOS-Practice on GitHub
What Changes in Swift 6
Swift 6 treats data race warnings as ERRORS. Code that compiles today may not compile tomorrow:
// Swift 5: Warning
// Swift 6: Error
var count = 0
Task {
count += 1 // Mutation of captured var in @Sendable closure
}
Enable Strict Checking Now
In Xcode Build Settings:
- Set "Strict Concurrency Checking" to "Complete"
Or in Package.swift:
.target(
name: "MyTarget",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
This gives you Swift 6 behavior as warnings today.
Common Issues and Fixes
1. Captured Mutable Variables
// ❌ Problem
var results: [String] = []
await withTaskGroup(of: String.self) { group in
for item in items {
group.addTask {
results.append(process(item)) // Captured var mutation
}
}
}
// ✅ Fix: Collect results properly
let results = await withTaskGroup(of: String.self) { group in
for item in items {
group.addTask { process(item) }
}
return await group.reduce(into: []) { $0.append($1) }
}
2. Non-Sendable Types Crossing Boundaries
// ❌ Problem
class UserManager { // Not Sendable
var currentUser: User?
}
let manager = UserManager()
Task {
manager.currentUser = user // Passing non-Sendable across boundary
}
// ✅ Fix 1: Make it an actor
actor UserManager {
var currentUser: User?
func setUser(_ user: User) {
currentUser = user
}
}
// ✅ Fix 2: Make it Sendable
final class UserManager: Sendable {
let currentUser: User // Immutable = Sendable
}
3. Closures Capturing self
// ❌ Problem
class ViewModel {
var data: [Item] = []
func load() {
Task {
self.data = await fetchItems() // self isn't Sendable
}
}
}
// ✅ Fix: Use @MainActor
@MainActor
class ViewModel: ObservableObject {
@Published var data: [Item] = []
func load() async {
data = await fetchItems() // Actor-isolated, no capture issue
}
}
4. Global Mutable State
// ❌ Problem
var globalCache: [String: Data] = [:] // Global mutable state
// ✅ Fix: Use actor
actor GlobalCache {
static let shared = GlobalCache()
private var storage: [String: Data] = [:]
func get(_ key: String) -> Data? { storage[key] }
func set(_ key: String, _ value: Data) { storage[key] = value }
}
Migration Strategy
Step 1: Enable Warnings
Turn on strict checking, see what breaks.
Step 2: Fix Low-Hanging Fruit
- Add
Sendableto simple value types - Mark ViewModels with
@MainActor - Use
letinstead ofvarwhere possible
Step 3: Audit Reference Types
For each class, decide:
- Make it an actor?
- Make it immutable (
final+letonly)? - Add manual synchronization (
@unchecked Sendable)?
Step 4: Review Closures
Every closure passed to Task, actors, or async functions needs scrutiny.
Sendable Conformance Checklist
// ✅ Automatically Sendable
struct Point: Sendable { let x, y: Double }
enum Status: Sendable { case loading, loaded, error }
// ✅ Manually Sendable (immutable class)
final class Config: Sendable {
let apiKey: String
init(apiKey: String) { self.apiKey = apiKey }
}
// ✅ Actor (always Sendable)
actor DataStore { var items: [Item] = [] }
// ⚠️ Requires manual thread safety
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Any] = [:]
// ... synchronized access methods
}
@preconcurrency for Legacy Code
When using frameworks that aren't updated yet:
@preconcurrency import SomeOldFramework
// Suppresses Sendable warnings for this import
Use sparingly—it's a temporary escape hatch.
Testing Your Migration
// This test verifies your types work across concurrency boundaries
func testSendable() async {
let viewModel = await MainActor.run { MyViewModel() }
await withTaskGroup(of: Void.self) { group in
group.addTask { await viewModel.load() }
group.addTask { await viewModel.refresh() }
}
// If this compiles with strict checking, you're good
}
Interview Tip
Show awareness of the migration: "I've been preparing for Swift 6 by enabling strict concurrency checking. The main changes are ensuring types are Sendable, using actors for shared mutable state, and marking ViewModels with @MainActor. It's better to fix these warnings now than face errors later."