Loading...
Loading...
Write, review, or fix Swift concurrency code using actors, async/await, and structured concurrency. Use when implementing concurrent features, resolving data race warnings, migrating from GCD, enabling Swift 6 strict concurrency mode, or adopting Swift 6.2 approachable concurrency (@concurrent, main-actor-by-default, isolated conformances).
npx skill4agent add kmshdev/claude-swift-toolkit swift-concurrencyswift-networkingIs it UI code (views, view models, UI state)?
→ Yes → @MainActor
Does it manage shared mutable state?
→ Yes → Custom actor
Is it pure computation with no shared state?
→ Yes → nonisolated (Swift 6.2: @concurrent for CPU-heavy work)
Is it a one-off async operation from synchronous code?
→ Yes → Task { } (inherits actor context)// On the entire class (recommended for ViewModels)
@Observable @MainActor
class UserViewModel {
var users: [User] = []
var isLoading = false
func loadUsers() async {
isLoading = true
defer { isLoading = false }
users = try? await api.fetchUsers() ?? []
}
}
// On specific methods only
class DataProcessor {
@MainActor func updateUI(with results: [Result]) {
// Safe to touch UI state
}
nonisolated func processInBackground(_ data: Data) -> [Result] {
// Runs on any thread, no actor isolation
}
}actor ImageCache {
private var cache: [URL: Image] = [:]
func image(for url: URL) -> Image? {
cache[url]
}
func store(_ image: Image, for url: URL) {
cache[url] = image
}
}
// Usage — every call crosses an isolation boundary
let cache = ImageCache()
let image = await cache.image(for: url) // await requiredactor BankAccount {
var balance: Decimal
func withdraw(_ amount: Decimal) async throws {
// WARNING: balance might change between check and deduction
// because another task could run during the await
guard balance >= amount else { throw BankError.insufficientFunds }
await recordTransaction(amount) // suspension point — other tasks can interleave
// Re-check invariant after suspension
guard balance >= amount else { throw BankError.insufficientFunds }
balance -= amount
}
}await@Model@ModelModelContext@MainActorModelContainer// WRONG — passing @Model across actors
let item = context.fetch(...) // on MainActor
await backgroundActor.process(item) // non-Sendable error
// RIGHT — pass the identifier, re-fetch on the other side
let itemID = item.persistentModelID
await backgroundActor.process(itemID, container: container)
// In the background actor:
let bgContext = ModelContext(container)
let item = bgContext.model(for: itemID) as? Item@Observable @MainActor@MainActor.serialized@Suiteios-testing// Value types are automatically Sendable
struct UserID: Sendable { let rawValue: UUID }
// Immutable classes can be Sendable
final class Configuration: Sendable {
let apiURL: URL // let = immutable = safe
let timeout: TimeInterval
}
// Mutable shared state needs an actor, not @unchecked Sendable
// AVOID unless you truly manage synchronization yourself:
final class LegacyCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
// Must manually synchronize ALL access
}sendingfunc process(_ data: sending Data) async {
// Compiler guarantees caller won't access data after passing it
}async let profile = api.fetchProfile(id)
async let posts = api.fetchPosts(userId: id)
async let followers = api.fetchFollowers(userId: id)
let (p, po, f) = try await (profile, posts, followers) // all run in parallellet images = try await withThrowingTaskGroup(of: (URL, Image).self) { group in
for url in urls {
group.addTask {
let image = try await downloadImage(from: url)
return (url, image)
}
}
var results: [URL: Image] = [:]
for try await (url, image) in group {
results[url] = image
}
return results
}func processLargeDataset(_ items: [Item]) async throws {
for item in items {
try Task.checkCancellation() // Throws if cancelled
await process(item)
}
}
// Or check without throwing
if Task.isCancelled { return }// Task { } — inherits current actor context
@MainActor class ViewModel {
func start() {
Task {
// This runs on MainActor (inherited)
await loadData()
}
}
}
// Task.detached { } — no actor inheritance, runs on global executor
Task.detached(priority: .background) {
// This does NOT run on MainActor
let result = heavyComputation(data)
await MainActor.run { self.updateUI(result) }
}Task { }Task.detached { }| GCD Pattern | Modern Equivalent |
|---|---|
| |
| |
| |
| Actor isolation (semaphores + async = deadlock risk) |
| Actor |
| Completion handler | |
func legacyFetch(completion: @escaping (Result<Data, Error>) -> Void) { /* ... */ }
// Wrap with continuation
func modernFetch() async throws -> Data {
try await withCheckedThrowingContinuation { continuation in
legacyFetch { result in
continuation.resume(with: result) // MUST be called exactly once
}
}
}// Enable per-target in Package.swift
.target(name: "MyApp", swiftSettings: [.swiftLanguageMode(.v6)])
// Or per-file: suppress specific warnings during migration
@preconcurrency import SomeOldLibrary| Warning | Fix |
|---|---|
| "Sending 'value' risks data race" | Make type |
| "Non-sendable type captured by @Sendable closure" | Move to actor-isolated method or make type Sendable |
| "Actor-isolated property cannot be mutated from nonisolated context" | Add |
| "Global variable is not concurrency-safe" | Make it |
| "Conformance of X to protocol Y crosses into main actor-isolated code" | Use isolated conformance: |
references/swift-6-2-changes.md| Feature | What It Does |
|---|---|
| Async stays on caller's actor | No implicit background hop — eliminates "Sending X risks data races" errors |
| Explicit opt-in to run on concurrent pool (replaces |
| Main-actor-by-default mode | Opt-in: all types in a target implicitly |
| Isolated conformances | |
references/swiftui-concurrency.mdShapeLayoutvisualEffectonGeometryChangeSendableFor a complete actor-based persistence pattern with file backing, see.swift-actor-persistence
actor UserRepository {
private let api: any APIClientProtocol
private var cache: [UserID: User] = [:]
func user(_ id: UserID) async throws -> User {
if let cached = cache[id] { return cached }
let user: User = try await api.request("/users/\(id.rawValue)")
cache[id] = user
return user
}
}struct UserView: View {
@State private var viewModel = UserViewModel()
var body: some View {
content
.task { await viewModel.load() } // auto-cancels on disappear
.task(id: selectedID) { await viewModel.load(selectedID) } // re-runs when ID changes
}
}Task.checkCancellation()Task.task.taskonAppear { Task { } }@MainActorDispatchSemaphorewithCheckedContinuation@unchecked Sendable@concurrentTask.detached@MainActorSendableswift-networkingswiftui-ui-patterns.task@MainActorSendableios-testing#expectawaitcode-analyzerswift-actor-persistence