From apple-kit-skills
Resolves Swift concurrency compiler errors like Sendable conformance, actor isolation warnings, and strict diagnostics. Adopts SE-0466 approachable concurrency, structured patterns, and data-race-safe async code for Swift 6+.
npx claudepluginhub dpearson2699/swift-ios-skills --plugin all-ios-skillsThis skill uses the workspace's default tool permissions.
Review, fix, and write concurrent Swift code targeting Swift 6.3+. Apply actor
Generates design tokens/docs from CSS/Tailwind/styled-components codebases, audits visual consistency across 10 dimensions, detects AI slop in UI.
Records polished WebM UI demo videos of web apps using Playwright with cursor overlay, natural pacing, and three-phase scripting. Activates for demo, walkthrough, screen recording, or tutorial requests.
Delivers idiomatic Kotlin patterns for null safety, immutability, sealed classes, coroutines, Flows, extensions, DSL builders, and Gradle DSL. Use when writing, reviewing, refactoring, or designing Kotlin code.
Review, fix, and write concurrent Swift code targeting Swift 6.3+. Apply actor isolation, Sendable safety, and modern concurrency patterns with minimal behavior changes.
When diagnosing a concurrency issue, follow this sequence:
@MainActor, custom actor,
nonisolated) and whether a default isolation mode is active.Prefer edits that preserve existing behavior while satisfying data-race safety.
| Situation | Recommended fix |
|---|---|
| UI-bound type | Annotate the type or relevant members with @MainActor. |
| Protocol conformance on MainActor type | Use an isolated conformance: extension Foo: @MainActor Proto. |
| Global / static state | Protect with @MainActor or move into an actor. |
| Background work needed | Use a @concurrent async function on a nonisolated type. |
| Sendable error | Prefer immutable value types. Add Sendable only when correct. |
| Cross-isolation callback | Use sending parameters (SE-0430) for finer control. |
@unchecked Sendable or nonisolated(unsafe) was added.Swift 6.2 introduces "approachable concurrency" -- a set of language changes that make concurrent code safer by default while reducing annotation burden.
With the -default-isolation MainActor compiler flag (or the Xcode 26
"Approachable Concurrency" build setting), all code in a module runs on
@MainActor by default unless explicitly opted out.
Effect: Eliminates most data-race safety errors for UI-bound code and
global/static state without writing @MainActor everywhere.
// 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()
}
}
When to use: Recommended for apps, scripts, and other executable targets where most code is UI-bound. Not recommended for library targets that should remain actor-agnostic.
Nonisolated async functions now stay on the caller's actor by default instead
of hopping to the global concurrent executor. This is the
nonisolated(nonsending) behavior.
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)
}
}
Use @concurrent to explicitly request background execution when needed.
@concurrent ensures a function always runs on the concurrent thread pool,
freeing the calling actor to run other tasks.
class 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
// ...
}
}
To move a function to a background thread:
nonisolated (or the function itself is).@concurrent to the function.async if not already asynchronous.await at call sites.nonisolated struct PhotoProcessor {
@concurrent
func process(data: Data) async -> ProcessedPhoto? { /* ... */ }
}
// Caller:
processedPhotos[item.id] = await PhotoProcessor().process(data: data)
Task.immediate starts executing synchronously on the current actor before
any suspension point, rather than being enqueued.
Task.immediate { await handleUserInput() }
Use for latency-sensitive work that should begin without delay. There is also
Task.immediateDetached which combines immediate start with detached semantics.
Observations { } provides async observation of @Observable types via
AsyncSequence, enabling transactional change tracking.
for await _ in Observations { model.count } {
print("Count changed to \(model.count)")
}
A conformance that needs MainActor state is called an isolated conformance. The compiler ensures it is only used in a matching isolation context.
protocol 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
}
}
If ImageExporter were nonisolated, adding a StickerModel would fail:
"Main actor-isolated conformance of 'StickerModel' to 'Exportable' cannot be
used in nonisolated context."
ContinuousClock and SuspendingClock now expose .epoch (SE-0473), enabling instant comparison and conversion between clock types.
let continuous = ContinuousClock()
let elapsed = continuous.now - continuous.epoch // Duration since system boot
@MainActor for all UI-touching code. No exceptions.nonisolated only for methods that access immutable (let) properties
or are pure computations.@concurrent to explicitly move work off the caller's actor.nonisolated(unsafe) unless you have proven internal
synchronization and exhausted all other options.NSLock, DispatchSemaphore) inside actors.Sendable when all stored
properties are Sendable.Sendable.@MainActor classes are implicitly Sendable. Do NOT add redundant
Sendable conformance.final with all stored properties let and
Sendable.@unchecked Sendable is a last resort. Document why the compiler cannot
prove safety.sending parameters (SE-0430) for finer-grained isolation control.@preconcurrency import only for third-party libraries you cannot
modify. Plan to remove it.defer blocks can now contain await (SE-0493). Use for async cleanup — closing connections, flushing buffers, or releasing resources that require an async call.
func fetchData() async throws -> Data {
let connection = try await openConnection()
defer { await connection.close() }
return try await connection.read()
}
Task: Unstructured, inherits caller context.
Task { await doWork() }
Task.detached: No inherited context. Use only when you explicitly need to break isolation inheritance.
Task.immediate: Starts immediately on current actor. Use for latency-sensitive work.
Task.immediate { await handleUserInput() }
async let: Fixed number of concurrent operations.
async let a = fetchA()
async let b = fetchB()
let result = try await (a, b)
TaskGroup: Dynamic number of concurrent operations.
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.isCancelled or call
try Task.checkCancellation() in loops..task modifier in SwiftUI -- it handles cancellation on view disappear.withTaskCancellationHandler for cleanup.deinit or onDisappear.Actors are reentrant. State can change across suspension points.
// 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 }
}
Use AsyncStream to bridge callback/delegate APIs:
let stream = AsyncStream<Location> { continuation in
let delegate = LocationDelegate { location in
continuation.yield(location)
}
continuation.onTermination = { _ in delegate.stop() }
delegate.start()
}
Use withCheckedContinuation / withCheckedThrowingContinuation for
single-value callbacks. Resume exactly once.
@Observable classes should be @MainActor for view models.@State to own an @Observable instance (replaces @StateObject).Observations { } (SE-0475) for async observation of @Observable
properties as an AsyncSequence.When actors are not the right fit — synchronous access, performance-critical paths, or bridging C/ObjC — use low-level synchronization primitives:
Mutex<Value> (iOS 18+, Synchronization module): Preferred lock for
new code. Stores protected state inside the lock. withLock { } pattern.OSAllocatedUnfairLock (iOS 16+, os module): Use when targeting
older iOS versions. Supports ownership assertions for debugging.Atomic<Value> (iOS 18+, Synchronization module): Lock-free atomics
for simple counters and flags. Requires explicit memory ordering.Key rule: Never put locks inside actors (double synchronization), and never
hold a lock across await (deadlock risk). See
references/synchronization-primitives.md for full API details, code examples,
and a decision guide for choosing locks vs actors.
@MainActor freezes UI.
Move to a @concurrent function.@MainActor. Only UI-touching code does.Sendable struct, not an actor.Task references and cancel them, or
use the .task view modifier.[weak self] when capturing self in
long-lived stored tasks.DispatchSemaphore.wait() in async code
will deadlock. Use structured concurrency instead.@MainActor and nonisolated properties in one
type. Isolate the entire type consistently.@MainActor func
over await MainActor.run { }.@MainActorSendable conformance is correct (no unjustified @unchecked)@preconcurrency imports are documented with removal plan@concurrent, not @MainActor.task modifier used in SwiftUI instead of manual Task management