Loading...
Loading...
Resolve Swift concurrency compiler errors, adopt Swift 6.2 approachable concurrency (SE-0466), and write data-race-safe async code. Use when fixing Sendable conformance errors, actor isolation warnings, or strict concurrency diagnostics; when adopting default MainActor isolation, @concurrent, nonisolated(nonsending), or Task.immediate; when designing actor-based architectures, structured concurrency with TaskGroup, or background work offloading; or when migrating from @preconcurrency to full Swift 6 strict concurrency.
npx skill4agent add dpearson2699/swift-ios-skills swift-concurrency@MainActoractornonisolated| Situation | Recommended fix |
|---|---|
| UI-bound type | Annotate the type or relevant members with |
| Protocol conformance on MainActor type | Use an isolated conformance: |
| Global / static state | Protect with |
| Background work needed | Use a |
| Sendable error | Prefer immutable value types. Add |
| Cross-isolation callback | Use |
@unchecked Sendablenonisolated(unsafe)-default-isolation MainActor@MainActor@MainActor// With default MainActor isolation enabled, these are implicitly @MainActor:
final class StickerLibrary {
static let shared = StickerLibrary() // safe -- on MainActor
var stickers: [Sticker] = []
}
final class StickerModel {
let photoProcessor = PhotoProcessor()
var selection: [PhotosPickerItem] = []
}
// Conformances are also implicitly isolated:
extension StickerModel: Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}nonisolated(nonsending)class PhotoProcessor {
func extractSticker(data: Data, with id: String?) async -> Sticker? {
// In Swift 6.2, this runs on the caller's actor (e.g., MainActor)
// instead of hopping to a background thread.
// ...
}
}
@MainActor
final class StickerModel {
let photoProcessor = PhotoProcessor()
func extractSticker(_ item: PhotosPickerItem) async throws -> Sticker? {
guard let data = try await item.loadTransferable(type: Data.self) else {
return nil
}
// No data race -- photoProcessor stays on MainActor
return await photoProcessor.extractSticker(data: data, with: item.itemIdentifier)
}
}@concurrent@concurrentclass PhotoProcessor {
var cachedStickers: [String: Sticker] = [:]
func extractSticker(data: Data, with id: String) async -> Sticker {
if let sticker = cachedStickers[id] { return sticker }
let sticker = await Self.extractSubject(from: data)
cachedStickers[id] = sticker
return sticker
}
@concurrent
static func extractSubject(from data: Data) async -> Sticker {
// Expensive image processing -- runs on background thread pool
// ...
}
}nonisolated@concurrentasyncawaitnonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { /* ... */ }
}
// Caller:
processedPhotos[item.id] = await PhotoProcessor().process(data: data)Task.immediateTask.immediate { await handleUserInput() }Task.immediateDetachedObservations { }@ObservableAsyncSequencefor await _ in Observations(tracking: { model.count }) {
print("Count changed to \(model.count)")
}weak letSendableprotocol Exportable {
func export()
}
// Isolated conformance: only usable on MainActor
extension StickerModel: @MainActor Exportable {
func export() {
photoProcessor.exportAsPNG()
}
}
@MainActor
struct ImageExporter {
var items: [any Exportable]
mutating func add(_ item: StickerModel) {
items.append(item) // OK -- ImageExporter is on MainActor
}
}ImageExporternonisolatedStickerModel@MainActornonisolatedlet@concurrentnonisolated(unsafe)NSLockDispatchSemaphoreSendableSendableSendable@MainActorSendableSendablefinalletSendable@unchecked Sendablesending@preconcurrency importTask { await doWork() }Task.immediate { await handleUserInput() }async let a = fetchA()
async let b = fetchB()
let result = try await (a, b)try await withThrowingTaskGroup(of: Item.self) { group in
for id in ids {
group.addTask { try await fetch(id) }
}
for try await item in group { process(item) }
}Task.isCancelledtry Task.checkCancellation().taskwithTaskCancellationHandlerdeinitonDisappear// WRONG: State may change during await
actor Counter {
var count = 0
func increment() async {
let current = count
await someWork()
count = current + 1 // BUG: count may have changed
}
}
// CORRECT: Mutate synchronously, no reentrancy risk
actor Counter {
var count = 0
func increment() { count += 1 }
}AsyncStreamlet stream = AsyncStream<Location> { continuation in
let delegate = LocationDelegate { location in
continuation.yield(location)
}
continuation.onTermination = { _ in delegate.stop() }
delegate.start()
}withCheckedContinuationwithCheckedThrowingContinuation@Observable@MainActor@State@Observable@StateObjectObservations { }@ObservableAsyncSequence@MainActor@concurrent@MainActorSendableTask.task[weak self]selfDispatchSemaphore.wait()@MainActornonisolated@MainActor funcawait MainActor.run { }@MainActorSendable@unchecked@preconcurrency@concurrent@MainActor.taskreferences/swift-6-2-concurrency.mdreferences/approachable-concurrency.mdreferences/swiftui-concurrency.md