All skills
dpearson2699 avatar

/swift-concurrency

@bac5344

Resolve Swift concurrency compiler errors, adopt 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.

Use this Skill: https://skilld.dev/gh/dpearson2699/swift-ios-skills/swift-concurrency

This session only. Nothing lands on disk.

referencesbridging-interop.md

≈2.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Bridging and Interop

Patterns for bridging callback-based, delegate-based, and GCD code into Swift Concurrency.

Contents

Checked Continuations

Use withCheckedContinuation (non-throwing) or withCheckedThrowingContinuation (throwing) to bridge completion-handler APIs into async/await. Available iOS 13+.

Docs: withCheckedContinuation · withCheckedThrowingContinuation

Basic Pattern

func fetchData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetch { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

Rules

  • Resume exactly once. Missing resume suspends the task forever (leak). Double resume crashes at runtime.
  • Prefer checked over unsafe. withCheckedContinuation detects misuse at runtime with diagnostics. Use withUnsafeContinuation only in performance-critical paths after correctness is proven.
  • Capture continuation carefully. The continuation escapes the closure — ensure all code paths resume it, including error and cancellation paths.

Delegate Bridging

class LocationBridge: NSObject, CLLocationManagerDelegate {
    private var continuation: CheckedContinuation<CLLocation, any Error>?
    private let manager = CLLocationManager()

    func requestLocation() async throws -> CLLocation {
        try await withCheckedThrowingContinuation { continuation in
            self.continuation = continuation
            manager.delegate = self
            manager.requestLocation()
        }
    }

    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        continuation?.resume(returning: locations[0])
        continuation = nil
    }

    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        continuation?.resume(throwing: error)
        continuation = nil
    }
}

Cancellation Support

func fetchWithCancellation() async throws -> Data {
    try await withTaskCancellationHandler {
        try await withCheckedThrowingContinuation { continuation in
            let task = legacyFetch { result in
                switch result {
                case .success(let data): continuation.resume(returning: data)
                case .failure(let error): continuation.resume(throwing: error)
                }
            }
            // Store task for cancellation
        }
    } onCancel: {
        // Cancel the underlying work
    }
}

AsyncStream from Callbacks

For APIs that deliver multiple values over time (delegates, NotificationCenter), use AsyncStream:

func locationUpdates() -> AsyncStream<CLLocation> {
    AsyncStream { continuation in
        let delegate = StreamingLocationDelegate(continuation: continuation)
        continuation.onTermination = { _ in
            delegate.stop()
        }
        delegate.start()
    }
}

GCD Migration

GCD Pattern Migration direction
DispatchQueue.main.async { } @MainActor isolation or MainActor.run { }
DispatchQueue.global().async { } Task { } or Task.detached { } (Swift 6.2: @concurrent)
DispatchGroup async let or TaskGroup
DispatchSemaphore Actor isolation or AsyncStream
DispatchWorkItem with cancel Task with task.cancel()
DispatchQueue serial queue actor
DispatchQueue.concurrentPerform when the surrounding API can become async withTaskGroup, usually with bounded/chunked child work
DispatchQueue.concurrentPerform for a measured synchronous CPU-bound parallel-for Keep concurrentPerform; follow the audit below
DispatchSource.makeTimerSource Task.sleep(for:) in a loop, or Clock

Synchronous parallel-for: concurrentPerform versus task groups

Apple documents DispatchQueue.concurrentPerform as an efficient synchronous parallel-for: it executes every iteration and waits for them all to finish before returning. A task group also waits for its child tasks, but its API is async. Use a task group when the surrounding operation can be asynchronous. Keep concurrentPerform when a caller must remain synchronous and measurement shows that independent, finite CPU work benefits from a parallel-for. Finite CPU computation does not by itself violate the cooperative executor's forward-progress requirement.

The API is declared @preconcurrency, but its closure parameter is @Sendable. Under Swift 6 complete checking, direct captures of both UnsafeBufferPointer and UnsafeMutableBufferPointer are rejected because neither buffer view is Sendable. When the compiler cannot express a manually proven pointer invariant, confine nonisolated(unsafe) to the local base-pointer bindings captured by the closure:

func doubled(_ input: UnsafeBufferPointer<Int>) -> [Int] {
    guard !input.isEmpty else { return [] }

    return Array(unsafeUninitializedCapacity: input.count) { output, initializedCount in

        nonisolated(unsafe) let inputBase = input.baseAddress!
        nonisolated(unsafe) let outputBase = output.baseAddress!

        // SAFETY: concurrentPerform joins before return. Iteration i reads only
        // inputBase[i] and initializes only outputBase[i]; the ranges do not
        // alias, both contain input.count elements, and both remain valid for
        // the entire loop.
        DispatchQueue.concurrentPerform(iterations: input.count) { index in
            outputBase.advanced(by: index).initialize(
                to: inputBase[index] * 2
            )
        }

        initializedCount = input.count
    }
}

Before accepting this opt-out, require one adjacent // SAFETY: proof that covers:

  • the actual index, stride, range, and bounds arithmetic;
  • every alias between captured pointers and why concurrent reads and writes do not conflict;
  • initialization versus mutation of each destination element;
  • pointer validity until the synchronous loop has joined.

Disjoint ranges are a nonconflicting-access invariant, not synchronization. Input/output aliasing is allowed only when the access proof remains nonconflicting. For a same-base in-place transform, prove that iteration i reads element i before writing element i, touches no other element, and that the read/write sets for iterations i and j do not overlap when i != j. Same pointer identity alone proves neither safety nor unsafety; shifted, neighboring, strided, or tiled access requires a fresh alias and range proof. Never widen the opt-out to a buffer view, enclosing type, or unrelated shared state.

concurrentPerform does not automatically participate in Swift task cancellation. If cancellation is required, design an explicit thread-safe signal and define partial-output semantics, or move the operation behind an async API.

Acceptance checks

Before retaining this carve-out:

  • benchmark the complete operation against the serial implementation on representative supported devices and workloads;
  • compare parallel output with the serial result, byte-for-byte when the operation permits;
  • avoid nested parallel loops unless separate measurement shows that the resulting oversubscription is beneficial.

These are engineering checks, not Apple API guarantees. See the supplemental Swift Forums discussion for the original strict-concurrency use case.

DispatchGroup → TaskGroup

// Before (GCD)
let group = DispatchGroup()
for url in urls {
    group.enter()
    fetch(url) { _ in group.leave() }
}
group.notify(queue: .main) { updateUI() }

// After (Swift Concurrency)
let results = await withTaskGroup(of: Data?.self) { group in
    for url in urls {
        group.addTask { try? await fetch(url) }
    }
    return await group.reduce(into: [Data]()) { if let d = $1 { $0.append(d) } }
}
updateUI(results)

Serial Queue → Actor

// Before
let serialQueue = DispatchQueue(label: "com.app.cache")
serialQueue.async { self.cache[key] = value }

// After
actor Cache {
    private var storage: [String: Data] = [:]
    func set(_ key: String, _ value: Data) { storage[key] = value }
    func get(_ key: String) -> Data? { storage[key] }
}

Source: SKILL.md on GitHub

2 warnings16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive documentation and best practices for Swift Concurrency (Swift 6.2+), including guidance on actor isolation, Sendable safety, and synchronization primitives. No security issues or malicious patterns were identified.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: MEDIUM · 1 issue

  • Runlayer6mo

    5 files scanned · No issues

  • ZeroLeaks5mo

    1 finding · Score: 69/100

Signed by skilld at bac5344. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 days ago.

Steadyupdated 2 months ago

README badge

README badge for dpearson2699/swift-ios-skills/swift-concurrency