Advanced Concurrency

Preparing for Swift 6 Strict Concurrency

January 19, 2026
6 min read
Featured image for blog post: 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 Sendable to simple value types
  • Mark ViewModels with @MainActor
  • Use let instead of var where possible

Step 3: Audit Reference Types

For each class, decide:

  • Make it an actor?
  • Make it immutable (final + let only)?
  • 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."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch