
Swift Concurrency
- 14.1k installs
- 1.6k repo stars
- Updated May 28, 2026
- avdlee/swift-concurrency-agent-skill
swift-concurrency is an agent skill that Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendabl.
About
Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compiler and linter warnings. --- name: swift-concurrency description: Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compiler and linter warnings. --- # Swift Concurrency ## Fast Path Before proposing a fix: 1. Analyze `Package.swift` or `.pbxproj` to determine Swift language mode, strict concurrency level, default isolation, and upcoming features. Do this always, not only for migration work. Capture the exact diagnostic and offending symbol. Determine the isolation boundary: `@MainActor`, custom actor, actor instance isolation, or `nonisolated`. Confirm whether the code is UI-bound or intended to run off the main actor. When spawning unstructured tasks, inspect the synchronous prefix (everything before the first `await`): start on `@MainActor` only when that prefix truly needs main-actor access; otherwise u.
- Capture the exact diagnostic and offending symbol.
- Determine the isolation boundary: `@MainActor`, custom actor, actor instance isolation, or `nonisolated`.
- Do not recommend `@MainActor` as a blanket fix. Justify why the code is truly UI-bound.
- Prefer structured concurrency over unstructured tasks. Use `Task.detached` only with a clear reason.
- If recommending `@preconcurrency`, `@unchecked Sendable`, or `nonisolated(unsafe)`, require a documented safety invarian
Swift Concurrency by the numbers
- 14,050 all-time installs (skills.sh)
- +306 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #52 of 2,184 Testing & QA skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
swift-concurrency capabilities & compatibility
- Capabilities
- capture the exact diagnostic and offending symbo · determine the isolation boundary: `@mainactor`, · do not recommend `@mainactor` as a blanket fix. · prefer structured concurrency over unstructured · if recommending `@preconcurrency`, `@unchecked s
- Use cases
- documentation
What swift-concurrency says it does
--- # Swift Concurrency ## Fast Path Before proposing a fix: 1.
Analyze `Package.swift` or `.pbxproj` to determine Swift language mode, strict concurrency level, default isolation, and upcoming features.
Do this always, not only for migration work.
Capture the exact diagnostic and offending symbol.
npx skills add https://github.com/avdlee/swift-concurrency-agent-skill --skill swift-concurrencyAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 14.1k |
|---|---|
| repo stars | ★ 1.6k |
| Security audit | 3 / 3 scanners passed |
| Last updated | May 28, 2026 |
| Repository | avdlee/swift-concurrency-agent-skill ↗ |
What problem does swift-concurrency solve for developers using this skill?
Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurr
Who is it for?
Developers who need swift-concurrency patterns described in the cached skill documentation.
Skip if: Skip when docs are empty or the task is outside the skill's documented scope.
When should I use this skill?
Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurr
What you get
Actionable workflows and conventions from SKILL.md for swift-concurrency.
- concurrency-correct Swift code patterns
- reference-guided fixes
By the numbers
- Six foundation reference modules: async-await-basics, tasks, actors, sendable, threading, plus stream modules
- Documents Swift 6.2 isolation behavior in threading reference
Files
Swift Concurrency
Fast Path
Before proposing a fix:
1. Analyze Package.swift or .pbxproj to determine Swift language mode, strict concurrency level, default isolation, and upcoming features. Do this always, not only for migration work. 2. Capture the exact diagnostic and offending symbol. 3. Determine the isolation boundary: @MainActor, custom actor, actor instance isolation, or nonisolated. 4. Confirm whether the code is UI-bound or intended to run off the main actor. When spawning unstructured tasks, inspect the synchronous prefix (everything before the first await): start on @MainActor only when that prefix truly needs main-actor access; otherwise use Task { @concurrent in ... } and hop back with MainActor.run only after the suspension. A trivial non-main line (for example, print) followed by main-actor work in the same prefix is not a reason to use @concurrent. For delayed retries, timers, and backoff tasks, separate the waiting from the UI mutation. The sleep often belongs off the main actor even when the final state update belongs on it.
Project settings that change concurrency behavior:
| Setting | SwiftPM (Package.swift) | Xcode (.pbxproj) |
|---|---|---|
| Language mode | swiftLanguageVersions or -swift-version (// swift-tools-version: is not a reliable proxy) | Swift Language Version |
| Strict concurrency | .enableExperimentalFeature("StrictConcurrency=targeted") | SWIFT_STRICT_CONCURRENCY |
| Default isolation | .defaultIsolation(MainActor.self) | SWIFT_DEFAULT_ACTOR_ISOLATION |
| Upcoming features | .enableUpcomingFeature("NonisolatedNonsendingByDefault") | SWIFT_UPCOMING_FEATURE_* |
| Approachable Concurrency | N/A (use individual upcoming features) | SWIFT_APPROACHABLE_CONCURRENCY |
Xcode 26 note: New projects created in Xcode 26 will often start withSWIFT_DEFAULT_ACTOR_ISOLATION = MainActorandSWIFT_APPROACHABLE_CONCURRENCY = YESenabled by default. Treat these as likely defaults for newly created projects, not as confirmed settings.
If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance. Do not guess, even for new Xcode 26 projects.
Guardrails:
- Do not recommend
@MainActoras a blanket fix. Justify why the code is truly UI-bound. - Prefer structured concurrency over unstructured tasks. Use
Task.detachedonly with a clear reason. - If recommending
@preconcurrency,@unchecked Sendable, ornonisolated(unsafe), require a documented safety invariant and a follow-up removal plan. - Optimize for the smallest safe change. Do not refactor unrelated architecture during migration.
- Course references are for deeper learning only. Use them sparingly and only when they clearly help answer the developer's question.
Quick Fix Mode
Use Quick Fix Mode when all of these are true:
- The issue is localized to one file or one type.
- The isolation boundary is clear.
- The fix can be explained in 1-2 behavior-preserving steps.
Skip Quick Fix Mode when any of these are true:
- Build settings or default isolation are unknown.
- The issue crosses module boundaries or changes public API behavior.
- The likely fix depends on unsafe escape hatches.
Common Diagnostics
| Diagnostic | First check | Smallest safe fix | Escalate to |
|---|---|---|---|
Main actor-isolated ... cannot be used from a nonisolated context | Is this truly UI-bound? | Isolate the caller to @MainActor or use await MainActor.run { ... } only when main-actor ownership is correct. | references/actors.md, references/threading.md |
Actor-isolated type does not conform to protocol | Must the requirement run on the actor? | Prefer isolated conformance (e.g., extension Foo: @MainActor SomeProtocol); use nonisolated only for truly nonisolated requirements. | references/actors.md |
Sending value of non-Sendable type ... risks causing data races | What isolation boundary is being crossed? | Keep access inside one actor, or convert the transferred value to an immutable/value type. | references/sendable.md, references/threading.md |
SwiftLint async_without_await | Is async actually required by protocol, override, or @concurrent? | Remove async, or use a narrow suppression with rationale. Never add fake awaits. | references/linting.md |
wait(...) is unavailable from asynchronous contexts | Is this legacy XCTest async waiting? | Replace with await fulfillment(of:) or Swift Testing equivalents. | references/testing.md |
| Core Data concurrency warnings | Are NSManagedObject instances crossing contexts or actors? | Pass NSManagedObjectID or map to a Sendable value type. | references/core-data.md |
Thread.current unavailable from asynchronous contexts | Are you debugging by thread instead of isolation? | Reason in terms of isolation and use Instruments/debugger instead. | references/threading.md |
| SwiftLint concurrency-related warnings | Which specific lint rule triggered? | Use references/linting.md for rule intent and preferred fixes; avoid dummy awaits. | references/linting.md |
... cannot satisfy conformance requirement for a 'Sendable' type parameter (SendableMetatype) | Does the conformance carry global-actor isolation? | Remove actor isolation from the conformance, or avoid passing the metatype across isolation boundaries. See SendableMetatype section in references/actors.md. | references/actors.md |
When Quick Fixes Fail
1. Gather project settings if not already confirmed. 2. Re-evaluate which isolation boundaries the type crosses. 3. Route to the matching reference file for a deeper fix. 4. If the fix may change behavior, document the invariant and add verification steps.
Smallest Safe Fixes
Prefer changes that preserve behavior while satisfying data-race safety:
- UI-bound state: isolate the type or member to
@MainActor. - Shared mutable state: move it behind an
actor, or use@MainActoronly if the state is UI-owned. - Background work: when work must hop off caller isolation, use an
asyncAPI marked@concurrent; when work can safely inherit caller isolation, usenonisolatedwithout@concurrent. When spawning aTask, match entry isolation to its synchronous prefix. If nothing before the firstawaitneeds the main actor, useTask { @concurrent in ... }and hop back viaawait MainActor.run { ... }for the UI update. If the prefix mixes a trivial non-main statement with main-actor work, keep the inherited@MainActorstart—splitting the cheap line off-main is not worth an extra hop. - Sendability issues: prefer immutable values and explicit boundaries over
@unchecked Sendable.
Concurrency Tool Selection
| Need | Tool | Key Guidance |
|---|---|---|
| Single async operation | async/await | Default choice for sequential async work |
| Fixed parallel operations | async let | Known count at compile time; auto-cancelled on throw |
| Dynamic parallel operations | withTaskGroup | Unknown count; structured — cancels children on scope exit |
| Sync → async bridge | Task { } | Inherits actor context; use Task.detached only with documented reason |
| Shared mutable state | actor | Prefer over locks/queues; keep isolated sections small |
| UI-bound state | @MainActor | Only for truly UI-related code; justify isolation |
Common Scenarios
Network request with UI update
Task { @concurrent in
let data = try await fetchData()
await MainActor.run { self.updateUI(with: data) }
}Processing array items in parallel
await withTaskGroup(of: ProcessedItem.self) { group in
for item in items {
group.addTask { await process(item) }
}
for await result in group {
results.append(result)
}
}Task entry isolation
Match a Task's entry isolation to its synchronous prefix (everything from { to the first await).
- If anything in that prefix needs
@MainActor, keep the inherited@MainActorstart. - If nothing in that prefix needs
@MainActor, preferTask { @concurrent in ... }and hop back only for UI-owned mutation.
// ❌ Synchronous prefix is empty; first work hops away
Task {
await hopToOtherIsolationDomain()
}
// ❌ Synchronous prefix is only `print` (trivial, non-main); first await hops away
Task {
print("Also not main-thread-bound")
await hopToOtherIsolationDomain()
}
// ✅ Start off the main actor, hop back only for UI work
Task { @concurrent in
await hopToOtherIsolationDomain()
await MainActor.run { updateUI() }
}
// ✅ Synchronous prefix DOES contain main-actor work — keep inheritance
Task {
print("debug") // trivial, non-main — rides along
self.isLoading = true // needs @MainActor, before any await
await fetchData()
}Swift 6 Migration Quick Guide
Key changes in Swift 6:
- Strict concurrency checking enabled by default
- Complete data-race safety at compile time
- Sendable requirements enforced on boundaries
- Isolation checking for all async boundaries
Migration Validation Loop
Apply this cycle for each migration change:
1. Build — Run swift build or Xcode build to surface new diagnostics 2. Fix — Address one category of error at a time (e.g., all Sendable issues first) 3. Rebuild — Confirm the fix compiles cleanly before moving on 4. Test — Run the test suite to catch regressions (swift test or Cmd+U) 5. Only proceed to the next file/module when all diagnostics are resolved
If a fix introduces new warnings, resolve them before continuing. Never batch multiple unrelated fixes — keep commits small and reviewable.
For detailed migration steps, see references/migration.md.
Reference Router
Open the smallest reference that matches the question:
- Foundations
references/async-await-basics.md— async/await syntax, execution order, async let, URLSession patternsreferences/tasks.md— Task lifecycle, cancellation, priorities, task groups, structured vs unstructuredreferences/actors.md— Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutexreferences/sendable.md— Sendable conformance, value/reference types, @unchecked, region isolationreferences/threading.md— Execution model, suspension points, Swift 6.2 isolation behavior- Streams
references/async-sequences.md— AsyncSequence, AsyncStream, when to use vs regular async methodsreferences/async-algorithms.md— Debounce, throttle, merge, combineLatest, channels, timers- Applied topics
references/testing.md— Swift Testing first, XCTest fallback, leak checksreferences/performance.md— Profiling with Instruments, reducing suspension points, execution strategiesreferences/memory-management.md— Retain cycles in tasks, memory safety patternsreferences/core-data.md— NSManagedObject sendability, custom executors, isolation conflicts- Migration and tooling
references/migration.md— Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migrationreferences/linting.md— Concurrency-focused lint rules and SwiftLintasync_without_await- Glossary
references/glossary.md— Quick definitions of core concurrency terms
Verification Checklist
When changing concurrency code:
1. Re-check build settings before interpreting diagnostics. 2. Build and clear one category of errors before moving on. Do not batch unrelated fixes into the same change. 3. Run tests, especially actor-, lifetime-, and cancellation-sensitive tests. 4. Use Instruments for performance claims instead of guessing. 5. Verify deallocation and cancellation behavior for long-lived tasks. 6. Check Task.isCancelled in long-running operations. 7. Never use semaphores or ad hoc locking in async contexts when actor isolation or Mutex would express ownership more safely.
---
Note: This skill is based on the comprehensive Swift Concurrency Course by Antoine van der Lee.
Reference Index
Quick navigation for the Swift Concurrency skill.
Foundations
| File | Use it for |
|---|---|
async-await-basics.md | closure-to-async bridges and foundational async/await usage |
tasks.md | Task, cancellation, task groups, structured vs unstructured work |
actors.md | actor isolation, @MainActor, reentrancy, isolated conformances |
sendable.md | Sendable, @Sendable, region isolation, escape hatches |
threading.md | execution model, suspension points, Swift 6.2 isolation behavior |
Streams
| File | Use it for |
|---|---|
async-sequences.md | deciding between AsyncSequence, AsyncStream, and one-shot async APIs |
async-algorithms.md | debounce, throttle, merge, combineLatest, channels, timers |
Applied Topics
| File | Use it for |
|---|---|
testing.md | Swift Testing first, XCTest fallback, leak checks |
performance.md | Instruments workflow, actor hops, suspension cost |
memory-management.md | retain cycles, long-lived tasks, cleanup |
core-data.md | NSManagedObjectID, perform, default isolation conflicts |
Migration and Tooling
| File | Use it for |
|---|---|
migration.md | rollout order, build settings, migration guardrails |
linting.md | concurrency-focused lint rules |
glossary.md | quick definitions |
Problem Router
- "I need to fix a compiler error quickly" →
../SKILL.md - "I need to replace a callback with async/await" →
async-await-basics.md - "I need to protect shared mutable state" →
actors.md - "I need to pass data safely across boundaries" →
sendable.md - "I need stream operators" →
async-algorithms.md - "I need to understand why code runs where it runs" →
threading.md - "I need to stop a leak or lifetime issue" →
memory-management.md - "I need to migrate to Swift 6" →
migration.md - "I need to test async code" →
testing.md - "I need to optimize slow async code" →
performance.md
Actors
Use this when:
- You need to protect class-based mutable state from concurrent access.
- You are choosing between
actor,@MainActor,nonisolated, orMutex. - You are resolving protocol conformance issues on actor-isolated types.
Skip this file if:
- You mainly need to make a value safe to transfer across boundaries. Use
sendable.md. - You are debugging execution threads or suspension behavior. Use
threading.md.
Jump to:
- Actor Isolation
- Global Actors / @MainActor
- Isolated vs Nonisolated
- Actor Reentrancy
- Isolated Deinit / Isolated Conformances (Swift 6.2+)
#isolationMacro- Mutex: Alternative to Actors
- Decision Tree
What is an Actor?
Actors protect mutable state by ensuring only one task accesses it at a time. They're reference types with automatic synchronization.
actor Counter {
var value = 0
func increment() {
value += 1
}
}Key guarantee: Only one task can access mutable state at a time (serialized access).
Course Deep Dive: This topic is covered in detail in Lesson 5.1: Understanding actors in Swift Concurrency
Actor Isolation
Enforced by compiler
actor BankAccount {
var balance: Int = 0
func deposit(_ amount: Int) {
balance += amount
}
}
let account = BankAccount()
account.balance += 1 // ❌ Error: can't mutate from outside
await account.deposit(1) // ✅ Must use actor's methodsReading properties
let account = BankAccount()
await account.deposit(100)
print(await account.balance) // Must await reads tooAlways use await when accessing actor properties/methods—you don't know if another task is inside.
Actors vs Classes
Similarities
- Reference types (copies share same instance)
- Can have properties, methods, initializers
- Can conform to protocols
Differences
- No inheritance (except
NSObjectfor Objective-C interop) - Automatic isolation (no manual locks needed)
- Implicit Sendable conformance
// ❌ Can't inherit from actors
actor Base {}
actor Child: Base {} // Error
// ✅ NSObject exception
actor Example: NSObject {} // OK for Objective-CGlobal Actors
Shared isolation domain across types, functions, and properties.
@MainActor
Ensures execution on main thread:
@MainActor
final class ViewModel {
var items: [Item] = []
}
@MainActor
func updateUI() {
// Always runs on main thread
}
@MainActor
var title: String = ""Custom global actors
@globalActor
actor ImageProcessing {
static let shared = ImageProcessing()
private init() {} // Prevent duplicate instances
}
@ImageProcessing
final class ImageCache {
var images: [URL: Data] = [:]
}
@ImageProcessing
func applyFilter(_ image: UIImage) -> UIImage {
// All image processing serialized
}Use private init to prevent creating multiple executors.
Course Deep Dive: This topic is covered in detail in Lesson 5.2: An introduction to Global Actors
@MainActor Best Practices
When to use
UI-related code that must run on main thread:
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
}Replacing DispatchQueue.main
// Old way
DispatchQueue.main.async {
// Update UI
}
// Modern way
await MainActor.run {
// Update UI
}
// Better: Use attribute
@MainActor
func updateUI() {
// Automatically on main thread
}MainActor.assumeIsolated
Use sparingly - assumes you're on main thread, crashes if not:
func methodB() {
assert(Thread.isMainThread) // Validate assumption
MainActor.assumeIsolated {
someMainActorMethod()
}
}Prefer: Explicit @MainActor or await MainActor.run over assumeIsolated.
Course Deep Dive: This topic is covered in detail in Lesson 5.3: When and how to use @MainActor
Isolated vs Nonisolated
Default: Isolated
Actor methods are isolated by default:
actor BankAccount {
var balance: Double
// Implicitly isolated
func deposit(_ amount: Double) {
balance += amount
}
}Isolated parameters
Reduce suspension points by inheriting caller's isolation:
struct Charger {
static func charge(
amount: Double,
from account: isolated BankAccount
) async throws -> Double {
// No await needed - we're isolated to account
try account.withdraw(amount: amount)
return account.balance
}
}Isolated closures
actor Database {
func transaction<T>(
_ operation: @Sendable (_ db: isolated Database) throws -> T
) throws -> T {
beginTransaction()
let result = try operation(self)
commitTransaction()
return result
}
}
// Usage: Multiple operations, one await
try await database.transaction { db in
db.insert(item1)
db.insert(item2)
db.insert(item3)
}Generic isolated extension
extension Actor {
func performInIsolation<T: Sendable>(
_ block: @Sendable (_ actor: isolated Self) throws -> T
) async rethrows -> T {
try block(self)
}
}
// Usage
try await bankAccount.performInIsolation { account in
try account.withdraw(amount: 20)
print("Balance: \(account.balance)")
}Nonisolated
Opt out of isolation for immutable data:
actor BankAccount {
let accountHolder: String
nonisolated var details: String {
"Account: \(accountHolder)"
}
}
// No await needed
print(account.details)Protocol conformance
extension BankAccount: CustomStringConvertible {
nonisolated var description: String {
"Account: \(accountHolder)"
}
}Course Deep Dive: This topic is covered in detail in Lesson 5.4: Isolated vs. non-isolated access in actors
Isolated Deinit (Swift 6.2+)
Clean up actor state on deallocation:
actor FileDownloader {
var downloadTask: Task<Void, Error>?
isolated deinit {
downloadTask?.cancel() // Can call isolated methods
}
}Requires: iOS 18.4+, macOS 15.4+
Course Deep Dive: This topic is covered in detail in Lesson 5.5: Using Isolated synchronous deinit
Global Actor Isolated Conformance (Swift 6.2+)
Protocol conformance respecting actor isolation:
@MainActor
final class PersonViewModel {
let id: UUID
var name: String
}
extension PersonViewModel: @MainActor Equatable {
static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool {
lhs.id == rhs.id && lhs.name == rhs.name
}
}Enable: InferIsolatedConformances upcoming feature.
Course Deep Dive: This topic is covered in detail in Lesson 5.6: Adding isolated conformance to protocols
SendableMetatype Error with Isolated Conformances
Isolated conformances cannot satisfy a SendableMetatype requirement. This surfaces when you pass MyClass.self to a generic function whose type parameter requires Sendable.
protocol P {
static func doSomething()
}
func doSomethingStatic<T: P & SendableMetatype>(_ type: T.Type) { } // explicitly requires a Sendable type/metatype
@MainActor
class C { }
extension C: @MainActor P {
static func doSomething() { }
}
@MainActor
func test(c: C) {
doSomethingStatic(C.self)
// ❌ main actor-isolated conformance of 'C' to 'P' cannot satisfy
// conformance requirement for a 'Sendable' type parameter
}Fix options:
1. Remove actor isolation from the original conformance if the protocol requirements don't access actor state:
@MainActor
class C: P {
nonisolated static func doSomething() { } // ✅ Non-isolated requirement on a non-isolated conformance
}2. Avoid passing the metatype across isolation boundaries — call the static method directly rather than routing through the generic function.
3. Make the generic function actor-aware so it accepts an isolated conformance (requires changing the callee's signature).
Actor Reentrancy
Critical: State can change between suspension points.
actor BankAccount {
var balance: Double
func deposit(amount: Double) async {
balance += amount
// ⚠️ Actor unlocked during await
await logActivity("Deposited \(amount)")
// ⚠️ Balance may have changed!
print("Balance: \(balance)")
}
}Problem
async let _ = account.deposit(50)
async let _ = account.deposit(50)
async let _ = account.deposit(50)
// May print same balance three times:
// Balance: 150
// Balance: 150
// Balance: 150Solution
Complete actor work before suspending:
func deposit(amount: Double) async {
balance += amount
print("Balance: \(balance)") // Before suspension
await logActivity("Deposited \(amount)")
}Rule: Don't assume state is unchanged after await.
Course Deep Dive: This topic is covered in detail in Lesson 5.7: Understanding actor reentrancy
#isolation Macro
Inherit caller's isolation for generic code:
extension Collection where Element: Sendable {
func sequentialMap<Result: Sendable>(
isolation: isolated (any Actor)? = #isolation,
transform: (Element) async -> Result
) async -> [Result] {
var results: [Result] = []
for element in self {
results.append(await transform(element))
}
return results
}
}
// Usage from @MainActor context
Task { @MainActor in
let names = ["Alice", "Bob"]
let results = await names.sequentialMap { name in
await process(name) // Inherits @MainActor
}
}Benefits: Avoids unnecessary suspensions, allows non-Sendable data.
Task Closures and Isolation Inheritance
When spawning unstructured Task closures that need to work with non-Sendable types, you must capture the isolation parameter to inherit the caller's isolation context.
Problem: Task closures are @Sendable, which prevents capturing non-Sendable types:
func process(delegate: NonSendableDelegate) {
Task {
delegate.doWork() // ❌ Error: capturing non-Sendable type
}
}Solution: Use #isolation parameter and capture it inside the Task:
func process(
delegate: NonSendableDelegate,
isolation: isolated (any Actor)? = #isolation
) {
Task {
_ = isolation // Forces capture, Task inherits caller's isolation
delegate.doWork() // ✅ Safe - running on caller's actor
}
}Why `_ = isolation` is required: Per SE-0420, Task closures only inherit isolation when "a non-optional binding of an isolated parameter is captured by the closure." The _ = isolation statement forces this capture. The capture list syntax [isolation] should work but currently does not.
When to use this pattern:
- Spawning
Tasks that work withnon-Sendabledelegate objects - Fire-and-forget async work that needs access to caller's state
- Bridging callback-based APIs to async streams while keeping delegates alive
Note: This pattern keeps the non-Sendable value alive and accessible within the Task. The Task runs on the caller's isolation domain, so no cross-isolation "sending" occurs.
Course Deep Dive: This topic is covered in detail in Lesson 5.8: Inheritance of actor isolation using the #isolation macro
Custom Actor Executors
Advanced: Control how actor schedules work.
Serial executor
final class DispatchQueueExecutor: SerialExecutor {
private let queue: DispatchQueue
init(queue: DispatchQueue) {
self.queue = queue
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let executor = asUnownedSerialExecutor()
queue.async {
unownedJob.runSynchronously(on: executor)
}
}
}
actor LoggingActor {
private let executor: DispatchQueueExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
executor.asUnownedSerialExecutor()
}
init(queue: DispatchQueue) {
executor = DispatchQueueExecutor(queue: queue)
}
}When to use
- Integration with legacy DispatchQueue-based code
- Specific thread requirements (e.g., C++ interop)
- Custom scheduling logic
Default executor is usually sufficient.
Course Deep Dive: This topic is covered in detail in Lesson 5.9: Using a custom actor executor
Mutex: Alternative to Actors
Synchronous locking without async/await overhead (iOS 18+, macOS 15+).
Basic usage
import Synchronization
final class Counter {
private let count = Mutex<Int>(0)
var currentCount: Int {
count.withLock { $0 }
}
func increment() {
count.withLock { $0 += 1 }
}
}Sendable access to non-Sendable types
final class TouchesCapturer: Sendable {
let path = Mutex<NSBezierPath>(NSBezierPath())
func storeTouch(_ point: NSPoint) {
path.withLock { path in
path.move(to: point)
}
}
}Error handling
func decrement() throws {
try count.withLock { count in
guard count > 0 else {
throw Error.reachedZero
}
count -= 1
}
}Mutex vs Actor
| Feature | Mutex | Actor |
|---|---|---|
| Synchronous | ✅ | ❌ (requires await) |
| Async support | ❌ | ✅ |
| Thread blocking | ✅ | ❌ (cooperative) |
| Fine-grained locking | ✅ | ❌ (whole actor) |
| Legacy code integration | ✅ | ❌ |
Use Mutex when:
- Need synchronous access
- Working with legacy non-async APIs
- Fine-grained locking required
- Low contention, short critical sections
Use Actor when:
- Can adopt async/await
- Need logical isolation
- Working in async context
Course Deep Dive: This topic is covered in detail in Lesson 5.10: Using a Mutex as an alternative to actors
Common Patterns
View model with @MainActor
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
items = try await api.fetchItems()
}
}Background processing with custom actor
@ImageProcessing
final class ImageProcessor {
func process(_ images: [UIImage]) async -> [UIImage] {
images.map { applyFilters($0) }
}
}Mixed isolation
actor DataStore {
private var items: [Item] = []
func add(_ item: Item) {
items.append(item)
}
nonisolated func itemCount() -> Int {
// ❌ Can't access items
return 0
}
}Transaction pattern
actor Database {
func transaction<T>(
_ operation: @Sendable (_ db: isolated Database) throws -> T
) throws -> T {
beginTransaction()
defer { commitTransaction() }
return try operation(self)
}
}Best Practices
1. Prefer actors over manual locks for async code 2. Use @MainActor for UI - all view models, UI updates 3. Minimize work in actors - keep critical sections short 4. Watch for reentrancy - don't assume state unchanged after await 5. Use nonisolated sparingly - only for truly immutable data 6. Avoid assumeIsolated - prefer explicit isolation 7. Custom executors are rare - default is usually best 8. Consider Mutex for sync code - when async overhead not needed 9. Complete actor work before suspending - prevent reentrancy bugs 10. Use isolated parameters - reduce suspension points
Decision Tree
Need thread-safe mutable state?
├─ Async context?
│ ├─ Single instance? → Actor
│ ├─ Global/shared? → Global Actor (@MainActor, custom)
│ └─ UI-related? → @MainActor
│
└─ Synchronous context?
├─ Can refactor to async? → Actor
├─ Legacy code integration? → Mutex
└─ Fine-grained locking? → MutexFurther Learning
For migration strategies, advanced patterns, and real-world examples, see Swift Concurrency Course.
AsyncAlgorithms Package
Use this when:
- You need time-based operators (debounce, throttle, timers).
- You need to combine multiple async sequences (merge, combineLatest, zip).
- You are migrating from Combine or RxSwift operators to Swift Concurrency equivalents.
Skip this file if:
- You need basic
AsyncStreambridging for callbacks or delegates. Useasync-sequences.md. - You are choosing between
Task,async let, or task groups. Usetasks.md.
Jump to:
- Quick Start
- Time-Based Operators
- Combining Operators
- Multi-Consumer Scenarios
- Combine Migration Guide
- Best Practices
---
Quick Start
Top 5 most common operators:
import AsyncAlgorithms
// 1. Debounce rapid inputs
for await query in searchQueryStream.debounce(for: .milliseconds(500)) {
await performSearch(query)
}
// 2. Throttle repeated actions
for await _ in buttonClicks.throttle(for: .seconds(1)) {
await performAction()
}
// 3. Merge multiple independent streams
for await message in chat1Messages.merge(chat2Messages) {
display(message)
}
// 4. Combine dependent values
for await (username, email) in usernameStream.combineLatest(emailStream) {
validateForm(username: username, email: email)
}
// 5. Zip paired operations
for await (image, metadata) in imageStream.zip(metadataStream) {
await cache(image: image, metadata: metadata)
}See: AsyncAlgorithms on GitHub
---
Overview & Installation
What is AsyncAlgorithms?
Extends Swift's AsyncSequence with time-based operators, stream combination tools, and multi-consumer primitives.
Use for:
- Time-based operations: debounce, throttle, timers
- Combining streams: merge, combineLatest, zip, chain
- Multi-consumer scenarios: AsyncChannel for backpressure
- Specific operators: removeDuplicates, chunks, adjacentPairs, compacted
Use standard library for:
- Bridging callbacks: AsyncStream
- Simple iteration: for await in sequence
- Single-value operations: async/await
Installation
dependencies: [
.package(url: "https://github.com/apple/swift-async-algorithms", from: "1.0.0")
]
targets: [
.target(
name: "MyTarget",
dependencies: [
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms")
]
)
]Import:
import AsyncAlgorithms---
Time-Based Operators
debounce(for:tolerance:clock:)
Wait for inactivity before emitting. Use for rapid inputs like search fields.
Example: ArticleSearcher
import AsyncAlgorithms
@Observable
final class ArticleSearcher {
@MainActor private(set) var results: [Article] = []
private var searchQueryContinuation: AsyncStream<String>.Continuation?
private lazy var searchQueryStream: AsyncStream<String> = {
AsyncStream { continuation in
searchQueryContinuation = continuation
}
}()
func search(_ query: String) {
searchQueryContinuation?.yield(query)
}
func startDebouncedSearch() {
Task { @MainActor in
for await query in searchQueryStream.debounce(for: .milliseconds(500)) {
self.results = []
self.results = await APIClient.searchArticles(query)
}
}
}
}Benefits: Automatic cancellation, backpressure, cleaner than manual Task.sleep.
❌ Anti-Pattern
// Bad: Every keystroke spawns new task
func search(_ query: String) {
Task {
try? await Task.sleep(for: .milliseconds(500))
await performSearch(query)
}
}Problem: Multiple tasks execute simultaneously, causing out-of-order results.
Solution: Use debounce() for automatic backpressure.
---
throttle(for:clock:reducing:)
Emit at most one value per interval. Use for repeated actions like button taps.
Example: Like Button
import AsyncAlgorithms
struct LikeButton: View {
@State private var tapStream = AsyncStream<Void> { continuation in
// Continuation stored externally
}
@State private var isLiked = false
var body: some View {
Button(action: {
tapStream.continuation?.yield()
}) {
Image(systemName: isLiked ? "heart.fill" : "heart")
}
.task {
await handleThrottledTaps()
}
}
private func handleThrottledTaps() async {
for await _ in tapStream.throttle(for: .seconds(1)) {
await toggleLike()
}
}
private func toggleLike() async {
isLiked.toggle()
await APIClient.updateLikeStatus(isLiked: isLiked)
}
}Understanding reducing Parameter
// .latest (default): Keep most recent value
for await value in events.throttle(for: .seconds(1)) {
process(value)
}
// .oldest: Keep first value
for await value in events.throttle(for: .seconds(1), reducing: .oldest) {
process(value)
}
// Custom: Sum all values
for await value in events.throttle(for: .seconds(1)) { $0 + $1 } {
process(value)
}---
AsyncTimerSequence
Emit values at regular intervals. Use for periodic refresh or countdown timers.
Example: Feed Refresh
import AsyncAlgorithms
@MainActor @Observable
final class FeedViewModel {
private(set) var articles: [Article] = []
private var refreshTask: Task<Void, Never>?
func startAutoRefresh() {
refreshTask = Task {
for await _ in AsyncTimerSequence(interval: .seconds(30)) {
await refreshFeed()
}
}
}
private func refreshFeed() async {
articles = await APIClient.fetchLatestArticles()
}
}❌ Anti-Pattern
// Bad: Manual timer implementation
func startTimer() {
Task {
while !Task.isCancelled {
performAction()
try? await Task.sleep(for: .seconds(1))
}
}
}Solution: Use AsyncTimerSequence.
---
Combining Operators
merge(_:...)
Combine sequences into one, emitting as they arrive. Stable operator ✅
Use for independent data sources that don't depend on each other.
Example: Multi-Room Chat
import AsyncAlgorithms
actor ChatManager {
private var messageContinuations: [String: AsyncStream<ChatMessage>.Continuation] = [:]
func getMessagesStream(roomID: String) -> AsyncStream<ChatMessage> {
AsyncStream { continuation in
messageContinuations[roomID] = continuation
}
}
func receiveMessage(_ message: ChatMessage) {
messageContinuations[message.roomID]?.yield(message)
}
func startMonitoring(rooms: [String]) -> AsyncStream<ChatMessage> {
let streams = rooms.map { getMessagesStream(roomID: $0) }
return streams.merge()
}
}
// Usage
let manager = ChatManager()
let mergedMessages = await manager.startMonitoring(rooms: ["general", "random"])
for await message in mergedMessages {
print("[\(message.roomID)] \(message.text)")
}Behavior: Values emit as they arrive from any source. Order interleaved by timing. Cancellation propagates to all sources.
---
combineLatest(_:...)
Combine sequences, emitting tuple when any source emits. Always uses latest values. Stable operator ✅
Use for dependent values that need synchronization.
Example: Form Validation
import AsyncAlgorithms
struct SignupForm: View {
@State private var usernameStream = AsyncStream<String> { /* ... */ }
@State private var emailStream = AsyncStream<String> { /* ... */ }
@State private var passwordStream = AsyncStream<String> { /* ... */ }
@State private var formState = FormState.incomplete
var body: some View {
Form {
TextField("Username", text: $username)
TextField("Email", text: $email)
SecureField("Password", text: $password)
}
.task {
await validateForm()
}
}
private func validateForm() async {
for await (username, email, password) in
usernameStream.combineLatest(emailStream, passwordStream)
{
formState = await validate(
username: username,
email: email,
password: password
)
}
}
}❌ Anti-Pattern
// Bad: Manual value combining
actor FormValidator {
private var currentUsername: String = ""
private var currentEmail: String = ""
func updateUsername(_ username: String) {
currentUsername = username
checkForm()
}
}Solution: Use combineLatest().
---
zip(_:...)
Combine sequences by pairing elements in order. Stable operator ✅
Example: Image + Metadata
import AsyncAlgorithms
struct ImageLoader {
func loadImagesWithMetadata(urls: [URL]) async throws -> [LoadedImage] {
let imageStream = AsyncThrowingStream<UIImage, Error> { continuation in
Task {
for url in urls {
let image = try await downloadImage(from: url)
continuation.yield(image)
}
continuation.finish()
}
}
let metadataStream = AsyncThrowingStream<ImageMetadata, Error> { continuation in
Task {
for url in urls {
let metadata = try await fetchMetadata(for: url)
continuation.yield(metadata)
}
continuation.finish()
}
}
var results: [LoadedImage] = []
for try await (image, metadata) in imageStream.zip(metadataStream) {
results.append(LoadedImage(image: image, metadata: metadata))
}
return results
}
}Behavior: Emits tuple when all sequences emit. Maintains order. Finishes when shortest sequence finishes.
---
chain(_:...)
Concatenate sequences sequentially. Stable operator ✅
Example: Paginated Loading
import AsyncAlgorithms
struct ArticlePaginator {
func loadAllArticles() -> AsyncStream<[Article]> {
AsyncStream { continuation in
Task {
var page = 1
var hasMore = true
while hasMore {
let articles = try await fetchPage(page: page)
continuation.yield(articles)
hasMore = articles.count == 20
page += 1
}
continuation.finish()
}
}
}
}
// Usage: Chain cache + network
for await articles in loadFromCacheStream().chain(loadFromNetworkStream()) {
display(articles)
}Behavior: Emits all values from first sequence before starting second.
---
Utility Operators
removeDuplicates()
Remove adjacent duplicates. Stable operator ✅
import AsyncAlgorithms
actor ChatHistory {
private var messageStream = AsyncStream<ChatMessage> { /* ... */ }
func getUniqueMessages() -> AsyncStream<ChatMessage> {
messageStream.removeDuplicates()
}
}---
chunks() and chunked()
Collect values into batches. Stable operator ✅
import AsyncAlgorithms
struct BatchProcessor {
func processLargeDataset(dataStream: AsyncStream<DataItem>) async {
for await batch in dataStream.chunks(count: 100) {
await processBatch(batch)
}
}
func chunkedByTime(dataStream: AsyncStream<DataItem>) async {
for await batch in dataStream.chunked(by: .seconds(5)) {
await processBatch(batch)
}
}
}---
compacted() and adjacentPairs()
import AsyncAlgorithms
// Remove nil values
for await value in optionalValuesStream.compacted() {
process(value)
}
// Pair adjacent elements
for await (previous, current) in valuesStream.adjacentPairs() {
let difference = current - previous
}---
Multi-Consumer Scenarios
AsyncChannel
AsyncSequence with backpressure. Stable operator ✅
Use for producer-consumer patterns with flow control.
Example: Message Queue
import AsyncAlgorithms
actor MessageQueue {
private let channel = AsyncChannel<Message>()
func getMessages() -> AsyncStream<Message> {
channel
}
func enqueue(_ message: Message) async {
await channel.send(message)
}
func startProcessing() {
Task {
for await message in channel {
await process(message)
}
}
}
}
// Multiple producers
let queue = MessageQueue()
Task { await queue.enqueue(Message(type: .userAction, content: "tap")) }
Task { await queue.enqueue(Message(type: .network, content: "data")) }
queue.startProcessing()❌ Anti-Pattern
// Bad: Values split unpredictably
let stream = AsyncStream<Int> { continuation in
for i in 1...10 {
continuation.yield(i)
}
continuation.finish()
}
Task { for await value in stream { print("Consumer 1: \(value)") } }
Task { for await value in stream { print("Consumer 2: \(value)") } }Problem: Each value goes to only one consumer.
Solution: Use AsyncChannel for multi-consumer scenarios.
---
AsyncThrowingChannel
Like AsyncChannel but can emit errors. Stable operator ✅
Example: WebSocket
import AsyncAlgorithms
actor WebSocketConnection {
private let channel = AsyncThrowingChannel<WebSocketMessage, Error>()
func getMessages() -> AsyncThrowingStream<WebSocketMessage, Error> {
channel
}
func receiveMessage(_ message: WebSocketMessage) async {
await channel.send(message)
}
func reportError(_ error: Error) async {
await channel.finish(throwing: error)
}
}
// Usage
do {
for await message in connection.getMessages() {
handle(message)
}
} catch {
print("WebSocket error: \(error)")
}---
Combine Migration Guide
Operator Mapping Table
| Combine | AsyncAlgorithms | Status | Alternative |
|---|---|---|---|
.debounce() | debounce() | ✅ Stable | - |
.throttle() | throttle() | ✅ Stable | - |
.merge() | merge() | ✅ Stable | - |
.combineLatest() | combineLatest() | ✅ Stable | - |
.zip() | zip() | ✅ Stable | - |
.concat() | chain() | ✅ Stable | - |
.removeDuplicates() | removeDuplicates() | ✅ Stable | - |
.timer() | AsyncTimerSequence | ✅ Stable | - |
.share() | - | - | AsyncChannel |
.flatMap() | - | - | TaskGroup |
.receive(on:) | - | - | Task / @MainActor |
.eraseToAnyPublisher() | - | - | any AsyncSequence |
---
Migration Examples
Example 1: ArticleSearcher
Before: Combine
import Combine
final class ArticleSearcher: ObservableObject {
@Published private(set) var results: [Article] = []
@Published var searchQuery = ""
init() {
$searchQuery
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.removeDuplicates()
.flatMap { query in
APIClient.searchArticles(query)
.catch { _ in Just([]) }
}
.receive(on: DispatchQueue.main)
.assign(to: &$results)
}
}After: AsyncAlgorithms
import AsyncAlgorithms
@Observable
final class ArticleSearcher {
@MainActor private(set) var results: [Article] = []
private var searchQueryContinuation: AsyncStream<String>.Continuation?
private lazy var searchQueryStream: AsyncStream<String> = {
AsyncStream { continuation in
searchQueryContinuation = continuation
}
}()
func search(_ query: String) {
searchQueryContinuation?.yield(query)
}
func startDebouncedSearch() {
Task { @MainActor in
for await query in searchQueryStream
.debounce(for: .milliseconds(500))
.removeDuplicates()
{
do {
self.results = try await APIClient.searchArticles(query)
} catch {
self.results = []
}
}
}
}
}Benefits: Simpler error handling, no cancellables, automatic cancellation.
---
Example 2: Multi-Source Loading
Before: Combine Merge
import Combine
final class ArticleLoader: ObservableObject {
@Published private(set) var items: [Item] = []
func loadAllSources() {
let source1 = APIClient.fetchItems(from: .source1)
let source2 = APIClient.fetchItems(from: .source2)
Publishers.Merge(source1, source2)
.scan([]) { accumulated, new in
accumulated + new
}
.receive(on: DispatchQueue.main)
.assign(to: &$items)
}
}After: TaskGroup
import AsyncAlgorithms
@Observable
final class ArticleLoader {
@MainActor private(set) var items: [Item] = []
func loadAllSourcesParallel() async {
await withTaskGroup(of: [Item].self) { group in
group.addTask {
await APIClient.fetchItems(from: .source1)
}
group.addTask {
await APIClient.fetchItems(from: .source2)
}
for await newItems in group {
items.append(contentsOf: newItems)
}
}
}
}Key difference: For parallel execution, use TaskGroup instead of flatMap.
---
Example 3: Form Validation
Before: Combine
import Combine
final class FormValidator: ObservableObject {
@Published var username = ""
@Published var email = ""
@Published private(set) var formState: FormState = .incomplete
init() {
Publishers.CombineLatest2($username, $email)
.map { username, email in
validate(username: username, email: email)
}
.assign(to: &$formState)
}
}After: AsyncAlgorithms or async let
import AsyncAlgorithms
@Observable
final class FormValidator {
var username = ""
var email = ""
@MainActor private(set) var formState: FormState = .incomplete
// Option 1: combineLatest for stream-based validation
func startStreamValidation() {
Task { @MainActor in
for await (username, email) in
usernameStream.combineLatest(emailStream)
{
self.formState = validate(
username: username,
email: email
)
}
}
}
// Option 2: async let for simple validation
func validateForm() async {
let (username, email) = await (username, email)
formState = validate(
username: username,
email: email
)
}
}Choose:
combineLatest(): Continuous validation as fields changeasync let: One-time validation when all values available
---
Common Mistakes Agents Make
- Manual debounce with `Task.sleep`: This creates multiple concurrent tasks and risks out-of-order results. Use the stream-based
debounce(for:)operator from AsyncAlgorithms instead. - Sharing `AsyncStream` across multiple consumers: Values split unpredictably between consumers. Use
AsyncChannelfor multi-consumer scenarios with backpressure. Note:AsyncChannelis point-to-point, not broadcast like Combine's.share(). - Looking for a `.flatMap` equivalent: Use
TaskGroupfor fan-out; the semantics differ from Combine/RxflatMap. - Looking for `.receive(on:)` equivalent: Use
@MainActororTaskcontext for isolation instead.
Best Practices
1. Use time-based operators for rapid inputs: debounce() for search, throttle() for buttons 2. Combine streams with merge/combineLatest instead of manual state management 3. Use AsyncChannel for multi-consumer scenarios with backpressure 4. Ensure Sendable conformance when using operators across isolation boundaries 5. Leverage cancellation - Task cancellation propagates through all operators 6. Choose right tool: AsyncAlgorithms for complex streams, AsyncStream for bridging callbacks 7. Avoid manual sleep loops - use AsyncTimerSequence instead
---
Further Learning
- AsyncAlgorithms Documentation
- Combine Migration Guide
- Async Sequences
- Tasks - Task groups and structured concurrency
Async/Await Basics
Use this when:
- You are starting fresh with async/await and need foundational patterns.
- You are converting callback-based code to async/await.
- You need to understand execution order and the sync-to-async bridge.
Skip this file if:
- You need parallel execution with task groups or
async let. Usetasks.md. - You need stream-based async iteration. Use
async-sequences.md.
Jump to:
- Function Declaration
- Execution Order
- Parallel Execution with async let
- URLSession with Async/Await
- Migration Strategy
Function Declaration
Mark functions with async to indicate asynchronous work:
func fetchData() async -> Data {
// async work
}
func fetchData() async throws -> Data {
// async work that can fail
}Key benefit over closures: The compiler enforces return values. No forgotten completion handlers.
Course Deep Dive: This topic is covered in detail in Lesson 2.1: Introduction to async/await syntax
Calling Async Functions
From synchronous context
Use Task to bridge from sync to async:
Task {
let data = try await fetchData()
}From async context
Use await directly:
func processData() async throws {
let data = try await fetchData()
// process data
}Execution Order
Structured concurrency executes top-to-bottom in the order you expect:
let first = try await fetchData(1) // Waits for completion
let second = try await fetchData(2) // Starts after first completes
let third = try await fetchData(3) // Starts after second completesCode after await only executes once the awaited function returns.
Course Deep Dive: This topic is covered in detail in Lesson 2.2: Understanding the order of execution
Parallel Execution with async let
Use async let to run multiple operations concurrently:
async let data1 = fetchData(1)
async let data2 = fetchData(2)
async let data3 = fetchData(3)
let results = try await [data1, data2, data3]How async let works
- Starts immediately: The function executes right away, even before
await - Structured concurrency: Automatically canceled when leaving scope
- Error handling: If one fails, others are implicitly canceled when awaiting grouped results
- No redundant keywords: Don't use
try awaitin theasync letline itself
// Redundant - avoid this
async let data = try await fetchData()
// Correct - errors handled at await point
async let data = fetchData()
let result = try await dataWhen to use async let
Use when:
- Tasks don't depend on each other
- Number of tasks known at compile-time
- Want automatic cancellation on scope exit
Avoid when:
- Tasks must run sequentially
- Need dynamic task spawning (use
TaskGroup) - Need manual cancellation control
Limitations
- Cannot use at top-level declarations (only within function bodies)
- Tasks not explicitly awaited may be canceled implicitly
Course Deep Dive: This topic is covered in detail in Lesson 2.3: Calling async functions in parallel using async let
URLSession with Async/Await
URLSession provides async alternatives to closure-based APIs:
// Closure-based (old)
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else { return }
// handle response
}.resume()
// Async/await (modern)
let (data, response) = try await URLSession.shared.data(for: request)Benefits over closures
- No optional
dataorresponseto unwrap - Automatic error throwing
- Compiler enforces return values
- Simpler error handling with do-catch
Complete network request pattern
func fetchUser(id: Int) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
var request = URLRequest(url: url)
request.httpMethod = "GET"
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}POST requests with JSON
func createUser(_ user: User) async throws -> User {
let url = URL(string: "https://api.example.com/users")!
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try JSONEncoder().encode(user)
let (data, response) = try await URLSession.shared.data(for: request)
guard let httpResponse = response as? HTTPURLResponse,
(200...299).contains(httpResponse.statusCode) else {
throw NetworkError.invalidResponse
}
return try JSONDecoder().decode(User.self, from: data)
}Course Deep Dive: This topic is covered in detail in Lesson 2.4: Performing network requests using URLSession and async/await
Typed Errors (Swift 6)
Specify exact error types for better API contracts:
enum NetworkError: Error {
case invalidResponse
case decodingFailed(DecodingError)
case requestFailed(URLError)
}
func fetchData() async throws(NetworkError) -> Data {
do {
let (data, _) = try await URLSession.shared.data(from: url)
return data
} catch let error as URLError {
throw .requestFailed(error)
} catch {
throw .invalidResponse
}
}Callers know exactly which errors to handle.
Migration Strategy
When converting closure-based code:
1. Add new async method alongside old one - keeps code compiling 2. Update method signature - add async, remove completion parameter 3. Replace closure calls with await - use URLSession async APIs 4. Remove optional unwrapping - async APIs return non-optional values 5. Simplify error handling - use do-catch instead of nested closures 6. Return directly - compiler enforces return values
Common Patterns
Sequential execution (when order matters)
let user = try await fetchUser(id: 1)
let posts = try await fetchPosts(userId: user.id)
let comments = try await fetchComments(postIds: posts.map(\.id))Parallel execution (when independent)
async let user = fetchUser(id: 1)
async let settings = fetchSettings()
async let notifications = fetchNotifications()
let (userData, settingsData, notificationsData) = try await (user, settings, notifications)Mixed execution
// Fetch user first (required for next step)
let user = try await fetchUser(id: 1)
// Then fetch related data in parallel
async let posts = fetchPosts(userId: user.id)
async let followers = fetchFollowers(userId: user.id)
async let following = fetchFollowing(userId: user.id)
let profile = Profile(
user: user,
posts: try await posts,
followers: try await followers,
following: try await following
)Further Learning
For in-depth coverage of async/await patterns, error handling strategies, and real-world migration scenarios, see Swift Concurrency Course.
Async Sequences and Streams
Use this when:
- You need to iterate over values that arrive over time.
- You are bridging callback-based or delegate-based APIs to async/await.
- You need to choose between
AsyncSequence,AsyncStream, or a regular async method.
Skip this file if:
- You need time-based operators like debounce, throttle, or merge. Use
async-algorithms.md. - You are choosing between
Task,async let, or task groups. Usetasks.md.
Jump to:
- AsyncSequence Protocol
- AsyncStream / AsyncThrowingStream
- Bridging Callbacks and Delegates
- Stream Lifecycle and Cleanup
- Buffer Policies
- Standard Library Integration
- Limitations
- When to Use AsyncAlgorithms
AsyncSequence
Protocol for asynchronous iteration over values that become available over time.
Basic usage
for await value in someAsyncSequence {
print(value)
}Key difference from Sequence: Values may not all be available immediately.
Custom implementation
struct Counter: AsyncSequence, AsyncIteratorProtocol {
typealias Element = Int
let limit: Int
var current = 1
mutating func next() async -> Int? {
guard !Task.isCancelled else { return nil }
guard current <= limit else { return nil }
let result = current
current += 1
return result
}
func makeAsyncIterator() -> Counter {
self
}
}
// Usage
for await count in Counter(limit: 5) {
print(count) // 1, 2, 3, 4, 5
}Standard operators
Same functional operators as regular sequences:
// Filter
for await even in Counter(limit: 5).filter({ $0 % 2 == 0 }) {
print(even) // 2, 4
}
// Map
let mapped = Counter(limit: 5).map { $0 % 2 == 0 ? "Even" : "Odd" }
for await label in mapped {
print(label)
}
// Contains (awaits until found or sequence ends)
let contains = await Counter(limit: 5).contains(3) // trueTermination
Return nil from next() to end iteration:
mutating func next() async -> Int? {
guard !Task.isCancelled else {
return nil // Stop on cancellation
}
guard current <= limit else {
return nil // Stop at limit
}
return current
}Course Deep Dive: This topic is covered in detail in Lesson 6.1: Working with asynchronous sequences
AsyncStream
Convenient way to create async sequences without implementing protocols.
Basic creation
let stream = AsyncStream<Int> { continuation in
for i in 1...5 {
continuation.yield(i)
}
continuation.finish()
}
for await value in stream {
print(value)
}AsyncThrowingStream
For streams that can fail:
let throwingStream = AsyncThrowingStream<Int, Error> { continuation in
continuation.yield(1)
continuation.yield(2)
continuation.finish(throwing: SomeError())
}
do {
for try await value in throwingStream {
print(value)
}
} catch {
print("Error: \(error)")
}Course Deep Dive: This topic is covered in detail in Lesson 6.2: Using AsyncStream and AsyncThrowingStream in your code
Bridging Closures to Streams
Progress + completion handlers
// Old closure-based API
struct FileDownloader {
enum Status {
case downloading(Float)
case finished(Data)
}
func download(
_ url: URL,
progressHandler: @escaping (Float) -> Void,
completion: @escaping (Result<Data, Error>) -> Void
) throws {
// Implementation
}
}
// Modern stream-based API
extension FileDownloader {
func download(_ url: URL) -> AsyncThrowingStream<Status, Error> {
AsyncThrowingStream { continuation in
do {
try self.download(url, progressHandler: { progress in
continuation.yield(.downloading(progress))
}, completion: { result in
switch result {
case .success(let data):
continuation.yield(.finished(data))
continuation.finish()
case .failure(let error):
continuation.finish(throwing: error)
}
})
} catch {
continuation.finish(throwing: error)
}
}
}
}
// Usage
for try await status in downloader.download(url) {
switch status {
case .downloading(let progress):
print("Progress: \(progress)")
case .finished(let data):
print("Done: \(data.count) bytes")
}
}Simplified with Result
AsyncThrowingStream { continuation in
try self.download(url, progressHandler: { progress in
continuation.yield(.downloading(progress))
}, completion: { result in
continuation.yield(with: result.map { .finished($0) })
continuation.finish()
})
}Bridging Delegates
Location updates example
final class LocationMonitor: NSObject {
private var continuation: AsyncThrowingStream<CLLocation, Error>.Continuation?
let stream: AsyncThrowingStream<CLLocation, Error>
override init() {
var capturedContinuation: AsyncThrowingStream<CLLocation, Error>.Continuation?
stream = AsyncThrowingStream { continuation in
capturedContinuation = continuation
}
super.init()
self.continuation = capturedContinuation
locationManager.delegate = self
locationManager.startUpdatingLocation()
}
}
extension LocationMonitor: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
for location in locations {
continuation?.yield(location)
}
}
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
continuation?.finish(throwing: error)
}
}
// Usage
let monitor = LocationMonitor()
for try await location in monitor.stream {
print("Location: \(location.coordinate)")
}Stream Lifecycle
Termination callback
AsyncThrowingStream<Int, Error> { continuation in
continuation.onTermination = { @Sendable reason in
print("Terminated: \(reason)")
// Cleanup: remove observers, cancel work, etc.
}
continuation.yield(1)
continuation.finish()
}Termination reasons:
.finished- Normal completion.finished(Error?)- Completed with error (throwing stream).cancelled- Task canceled
Cancellation
Streams cancel when:
- Enclosing task cancels
- Stream goes out of scope
let task = Task {
for try await status in download(url) {
print(status)
}
}
task.cancel() // Triggers onTermination with .cancelledNo explicit cancel method - rely on task cancellation.
Buffer Policies
Control what happens to values when no one is awaiting:
.unbounded (default)
Buffers all values until consumed:
let stream = AsyncStream<Int> { continuation in
(0...5).forEach { continuation.yield($0) }
continuation.finish()
}
try await Task.sleep(for: .seconds(1))
for await value in stream {
print(value) // Prints all: 0, 1, 2, 3, 4, 5
}.bufferingNewest(n)
Keeps only the newest N values:
let stream = AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
(0...5).forEach { continuation.yield($0) }
continuation.finish()
}
try await Task.sleep(for: .seconds(1))
for await value in stream {
print(value) // Prints only: 5
}.bufferingOldest(n)
Keeps only the oldest N values:
let stream = AsyncStream(bufferingPolicy: .bufferingOldest(1)) { continuation in
(0...5).forEach { continuation.yield($0) }
continuation.finish()
}
try await Task.sleep(for: .seconds(1))
for await value in stream {
print(value) // Prints only: 0
}.bufferingNewest(0)
Only receives values emitted after iteration starts:
let stream = AsyncStream(bufferingPolicy: .bufferingNewest(0)) { continuation in
continuation.yield(1) // Discarded
Task {
try await Task.sleep(for: .seconds(2))
continuation.yield(2) // Received
continuation.finish()
}
}
try await Task.sleep(for: .seconds(1))
for await value in stream {
print(value) // Prints only: 2
}Use case: Location updates, file system changes - only care about latest.
Repeated Async Calls
Use init(unfolding:onCancel:) for polling:
struct PingService {
func startPinging() -> AsyncStream<Bool> {
AsyncStream {
try? await Task.sleep(for: .seconds(5))
return await ping()
} onCancel: {
print("Pinging cancelled")
}
}
func ping() async -> Bool {
// Network request
return true
}
}
// Usage
for await result in pingService.startPinging() {
print("Ping: \(result)")
}Standard Library Integration
NotificationCenter
let stream = NotificationCenter.default.notifications(
named: .NSSystemTimeZoneDidChange
)
for await notification in stream {
print("Time zone changed")
}Combine publishers
let numbers = [1, 2, 3, 4, 5]
let filtered = numbers.publisher.filter { $0 % 2 == 0 }
for await number in filtered.values {
print(number) // 2, 4
}Task groups
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
for await image in group {
display(image)
}
}Limitations
Single consumer only
Unlike Combine, streams support one consumer at a time:
let stream = AsyncStream { continuation in
(0...5).forEach { continuation.yield($0) }
continuation.finish()
}
Task {
for await value in stream {
print("Consumer 1: \(value)")
}
}
Task {
for await value in stream {
print("Consumer 2: \(value)")
}
}
// Unpredictable output - values split between consumers
// Consumer 1: 0
// Consumer 2: 1
// Consumer 1: 2
// Consumer 2: 3Solution: Create separate streams or use third-party libraries (AsyncExtensions).
No values after termination
Once finished, stream won't emit new values:
let stream = AsyncStream<Int> { continuation in
continuation.finish() // Terminate immediately
continuation.yield(1) // Never received
}
for await value in stream {
print(value) // Loop exits immediately
}Decision Guide
Use AsyncSequence when:
- Implementing standard library-style protocols
- Need fine-grained control over iteration
- Building reusable sequence types
- Working with existing sequence protocols
Reality: Rarely needed in application code.
Use AsyncStream when:
- Bridging delegates to async/await
- Converting closure-based APIs
- Emitting events manually
- Polling or repeated async operations
- Most common use case
---
When to Use AsyncAlgorithms vs Standard Library
Use AsyncAlgorithms when:
- Time-based operations need debounce/throttle/timer
- Combining multiple async sequences (merge, combineLatest, zip)
- Multi-consumer scenarios require backpressure (AsyncChannel)
- Complex operator chains that Combine would handle naturally
- Need specific operators not in standard library
Use Standard Library when:
- Bridging callback APIs → AsyncStream
- Simple iteration → for await in sequence
- Single-value operations → async/await
- Basic transformations → map/filter/contains
Quick Decision Table
| Need | Solution |
|---|---|
| Debounce search input | ✅ AsyncAlgorithms.debounce() |
| Throttle button clicks | ✅ AsyncAlgorithms.throttle() |
| Merge independent streams | ✅ AsyncAlgorithms.merge() |
| Combine dependent values | ✅ AsyncAlgorithms.combineLatest() or async let |
| Pair values from two sources | ✅ AsyncAlgorithms.zip() |
| Bridge callback API | AsyncStream |
| Multi-consumer with backpressure | ✅ AsyncChannel |
| Periodic timer | ✅ AsyncTimerSequence |
| Simple async iteration | for await in... |
See: async-algorithms.md for detailed usage examples with real-world patterns.
Use regular async methods when:
- Single value returned
- No progress updates needed
- Simple request/response pattern
// Use this
func fetchData() async throws -> Data
// Not this
func fetchData() -> AsyncThrowingStream<Data, Error>
> **Course Deep Dive**: This topic is covered in detail in [Lesson 6.3: Deciding between AsyncSequence, AsyncStream, or regular asynchronous methods](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)Common Patterns
Progress reporting
func download(_ url: URL) -> AsyncThrowingStream<DownloadEvent, Error> {
AsyncThrowingStream { continuation in
Task {
do {
var progress: Double = 0
while progress < 1.0 {
progress += 0.1
continuation.yield(.progress(progress))
try await Task.sleep(for: .milliseconds(100))
}
let data = try await URLSession.shared.data(from: url).0
continuation.yield(.completed(data))
continuation.finish()
} catch {
continuation.finish(throwing: error)
}
}
}
}Monitoring file system
func watchDirectory(_ path: String) -> AsyncStream<FileEvent> {
AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
let source = DispatchSource.makeFileSystemObjectSource(
fileDescriptor: fd,
eventMask: .write,
queue: .main
)
source.setEventHandler {
continuation.yield(.fileChanged(path))
}
continuation.onTermination = { _ in
source.cancel()
}
source.resume()
}
}Timer/polling
func timer(interval: Duration) -> AsyncStream<Date> {
AsyncStream { continuation in
Task {
while !Task.isCancelled {
continuation.yield(Date())
try? await Task.sleep(for: interval)
}
continuation.finish()
}
}
}
// Usage
for await date in timer(interval: .seconds(1)) {
print("Tick: \(date)")
}Best Practices
1. Always call finish() - Streams stay alive until terminated 2. Use buffer policies wisely - Match your use case (latest value vs all values) 3. Handle cancellation - Set onTermination for cleanup 4. Single consumer - Don't share streams across multiple consumers 5. Prefer streams over closures - More composable and cancellable 6. Check Task.isCancelled - Respect cancellation in custom sequences 7. Use throwing variant - When operations can fail 8. Consider regular async - If only returning single value
Debugging
Add termination logging
continuation.onTermination = { reason in
print("Stream ended: \(reason)")
}Validate finish() calls
// ❌ Forgot to finish
AsyncStream { continuation in
continuation.yield(1)
// Stream never ends!
}
// ✅ Always finish
AsyncStream { continuation in
continuation.yield(1)
continuation.finish()
}Check for dropped values
let stream = AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in
for i in 1...100 {
continuation.yield(i)
print("Yielded: \(i)")
}
continuation.finish()
}
// If consumer is slow, many values dropped
for await value in stream {
print("Received: \(value)")
try? await Task.sleep(for: .seconds(1))
}Common Mistakes Agents Make
// ❌ Values after finish() are silently dropped
continuation.finish()
continuation.yield(1) // Never received
// ❌ Stream never terminates (forgot finish)
AsyncStream { continuation in
continuation.yield(1)
// Missing: continuation.finish()
}
// ❌ Wrapping a single-value API in a stream — use a regular async function instead
func fetchUser() -> AsyncStream<User> { ... } // Overkill for one result- Sharing a single `AsyncStream` between multiple consumers: Values split unpredictably. There is no built-in broadcast; use
AsyncChannelfor point-to-point multi-consumer patterns. - Forgetting `onTermination` when bridging delegate or observer APIs, causing resources to leak.
Further Learning
For real-world migration examples, performance patterns, and advanced stream techniques, see Swift Concurrency Course.
Core Data and Swift Concurrency
Use this when:
- You need to use Core Data with async/await or actors.
NSManagedObjectinstances are crossing context or actor boundaries.- You are resolving default
@MainActorisolation conflicts with generated NSManagedObject subclasses.
Skip this file if:
- The issue is general actor isolation, not Core Data specific. Use
actors.md. - You need general Sendable guidance. Use
sendable.md.
Jump to:
- Core Principles
- Data Access Objects (DAO) Pattern
- Working Without DAOs (NSManagedObjectID)
- Bridging Closures to Async
- Custom Actor Executor (Advanced)
- Default MainActor Isolation
- SwiftUI Integration
- Common Mistakes
Core Principles
Thread safety still matters
Core Data's thread safety rules don't change with Swift Concurrency:
- Can't pass
NSManagedObjectbetween threads - Must access objects on their context's thread
NSManagedObjectIDis thread-safe (can pass around)
NSManagedObject cannot be Sendable
@objc(Article)
public class Article: NSManagedObject {
@NSManaged public var title: String // ❌ Mutable, can't be Sendable
}Don't use `@unchecked Sendable` - hides warnings without fixing safety.
Course Deep Dive: This topic is covered in detail in Lesson 9.1: An introduction to Swift Concurrency and Core Data
Available Async APIs
Context perform
extension NSManagedObjectContext {
func perform<T>(
schedule: ScheduledTaskType = .immediate,
_ block: @escaping () throws -> T
) async rethrows -> T
}What's missing
No async alternative for:
func loadPersistentStores(
completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void
)Must bridge manually (see below).
Data Access Objects (DAO)
Thread-safe value types representing managed objects.
Pattern
// Managed object (not Sendable)
@objc(Article)
public class Article: NSManagedObject {
@NSManaged public var title: String?
@NSManaged public var timestamp: Date?
}
// DAO (Sendable)
struct ArticleDAO: Sendable, Identifiable {
let id: NSManagedObjectID
let title: String
let timestamp: Date
init?(managedObject: Article) {
guard let title = managedObject.title,
let timestamp = managedObject.timestamp else {
return nil
}
self.id = managedObject.objectID
self.title = title
self.timestamp = timestamp
}
}Benefits
- Sendable: Safe to pass across isolation domains
- Immutable: No accidental mutations
- Clear API: Explicit data transfer
Drawbacks
- Requires rewrite: All fetch/mutation logic
- Boilerplate: DAO for each entity
- Complexity: Additional layer of abstraction
Course Deep Dive: This topic is covered in detail in Lesson 9.2: Sendable and NSManageObjects
Working Without DAOs
Pass only NSManagedObjectID between contexts.
Basic pattern
@MainActor
func fetchArticle(id: NSManagedObjectID) -> Article? {
viewContext.object(with: id) as? Article
}
func processInBackground(articleID: NSManagedObjectID) async throws {
let backgroundContext = container.newBackgroundContext()
try await backgroundContext.perform {
guard let article = backgroundContext.object(with: articleID) as? Article else {
return
}
// Process article
try backgroundContext.save()
}
}NSManagedObjectID is Sendable
// Safe to pass between tasks
let articleID = article.objectID
Task {
await processInBackground(articleID: articleID)
}Bridging Closures to Async
Load persistent stores
extension NSPersistentContainer {
func loadPersistentStores() async throws {
try await withCheckedThrowingContinuation { continuation in
self.loadPersistentStores { description, error in
if let error {
continuation.resume(throwing: error)
} else {
continuation.resume(returning: ())
}
}
}
}
}
// Usage
try await container.loadPersistentStores()Simple CoreDataStore Pattern
Enforce isolation at API level:
nonisolated struct CoreDataStore {
static let shared = CoreDataStore()
let persistentContainer: NSPersistentContainer
private var viewContext: NSManagedObjectContext {
persistentContainer.viewContext
}
private init() {
persistentContainer = NSPersistentContainer(name: "MyApp")
persistentContainer.viewContext.automaticallyMergesChangesFromParent = true
Task { [persistentContainer] in
try? await persistentContainer.loadPersistentStores()
}
}
// View context operations (main thread)
@MainActor
func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows {
try block(viewContext)
}
// Background operations
@concurrent
func performInBackground<T>(
_ block: @escaping (NSManagedObjectContext) throws -> T
) async rethrows -> T {
let context = persistentContainer.newBackgroundContext()
return try await context.perform {
try block(context)
}
}
}Usage
// Main thread operations
@MainActor
func loadArticles() throws -> [Article] {
try CoreDataStore.shared.perform { context in
let request = Article.fetchRequest()
return try context.fetch(request)
}
}
// Background operations
func deleteAll() async throws {
try await CoreDataStore.shared.performInBackground { context in
let request = Article.fetchRequest()
let articles = try context.fetch(request)
articles.forEach { context.delete($0) }
try context.save()
}
}Why this pattern works
- @MainActor: Enforces view context on main thread
- @concurrent: Forces background execution
- Compile-time safety: Wrong isolation = error
- Simple: No custom executors needed
Custom Actor Executor (Advanced)
Note: Usually not needed. Consider simple pattern first.
Course Deep Dive: This topic is covered in detail in Lesson 9.3: Using a custom Actor executor for Core Data (advanced)
Implementation
final class NSManagedObjectContextExecutor: @unchecked Sendable, SerialExecutor {
private let context: NSManagedObjectContext
init(context: NSManagedObjectContext) {
self.context = context
}
func enqueue(_ job: consuming ExecutorJob) {
let unownedJob = UnownedJob(job)
let executor = asUnownedSerialExecutor()
context.perform {
unownedJob.runSynchronously(on: executor)
}
}
func asUnownedSerialExecutor() -> UnownedSerialExecutor {
UnownedSerialExecutor(ordinary: self)
}
}Actor usage
actor CoreDataStore {
let persistentContainer: NSPersistentContainer
nonisolated let modelExecutor: NSManagedObjectContextExecutor
nonisolated var unownedExecutor: UnownedSerialExecutor {
modelExecutor.asUnownedSerialExecutor()
}
private init() {
persistentContainer = NSPersistentContainer(name: "MyApp")
let context = persistentContainer.newBackgroundContext()
modelExecutor = NSManagedObjectContextExecutor(context: context)
}
func deleteAll<T: NSManagedObject>(
using request: NSFetchRequest<T>
) throws {
let objects = try context.fetch(request)
objects.forEach { context.delete($0) }
try context.save()
}
}Drawbacks
- Hidden complexity: Executor details obscure Core Data
- Forces concurrency: Even for main thread operations
- Not simpler: More code than
perform { } - Error prone: Easy to use wrong context
Recommendation: Use simple pattern instead.
Default MainActor Isolation
Problem with auto-generated code
When default isolation set to @MainActor, auto-generated managed objects conflict:
// Auto-generated (can't modify)
class Article: NSManagedObject {
// Inherits @MainActor, conflicts with NSManagedObject
}Error: Main actor-isolated initializer has different actor isolation from nonisolated overridden declaration
Solution: Manual code generation
1. Set entity to "Manual/None" code generation 2. Generate class definitions 3. Mark as nonisolated:
nonisolated class Article: NSManagedObject {
@NSManaged public var title: String?
@NSManaged public var timestamp: Date?
}
> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.4: Autogenerated Core Data Objects and Default MainActor Isolation Conflicts](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)Benefit: Full control over isolation.
Common Patterns
Fetch on main thread
@MainActor
func fetchArticles() throws -> [Article] {
let request = Article.fetchRequest()
return try viewContext.fetch(request)
}Background save
func saveInBackground() async throws {
let context = container.newBackgroundContext()
try await context.perform {
let article = Article(context: context)
article.title = "New Article"
try context.save()
}
}Pass ID, fetch in context
@MainActor
func displayArticle(id: NSManagedObjectID) {
guard let article = viewContext.object(with: id) as? Article else {
return
}
// Use article
}
func processArticle(id: NSManagedObjectID) async throws {
try await CoreDataStore.shared.performInBackground { context in
guard let article = context.object(with: id) as? Article else {
return
}
// Process article
try context.save()
}
}Batch operations
@concurrent
func deleteAllArticles() async throws {
try await CoreDataStore.shared.performInBackground { context in
let request = NSFetchRequest<NSFetchRequestResult>(entityName: "Article")
let deleteRequest = NSBatchDeleteRequest(fetchRequest: request)
try context.execute(deleteRequest)
}
}SwiftUI Integration
Environment injection
@main
struct MyApp: App {
let persistentContainer = NSPersistentContainer(name: "MyApp")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
}
}
}View usage
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Article.timestamp, ascending: true)]
) private var articles: FetchedResults<Article>
var body: some View {
List(articles) { article in
Text(article.title ?? "")
}
}
}Best Practices
1. Pass NSManagedObjectID only - never managed objects 2. Use perform { } - don't access context directly 3. @MainActor for view context - enforce main thread 4. @concurrent for background - force background execution 5. Manual code generation - control isolation 6. Keep it simple - avoid custom executors unless needed 7. Enable Core Data debugging - catch thread violations 8. Merge changes automatically - automaticallyMergesChangesFromParent = true 9. Use background contexts - for heavy operations 10. Test with Thread Sanitizer - catch violations early
Debugging
Enable Core Data concurrency debugging
// Launch argument
-com.apple.CoreData.ConcurrencyDebug 1Crashes immediately on thread violations.
Thread Sanitizer
Enable in scheme settings to catch data races.
Assertions
@MainActor
func fetchArticles() -> [Article] {
assert(Thread.isMainThread)
// Fetch from viewContext
}Decision Tree
Need to access Core Data?
├─ UI/View context?
│ └─ Use @MainActor + viewContext
│
├─ Background operation?
│ ├─ Quick operation? → perform { } on background context
│ └─ Batch operation? → NSBatchDeleteRequest/NSBatchUpdateRequest
│
├─ Pass between contexts?
│ └─ Use NSManagedObjectID only
│
└─ Need Sendable type?
├─ Can refactor? → Use DAO pattern
└─ Can't refactor? → Pass NSManagedObjectIDMigration Strategy
For existing projects
1. Enable manual code generation for all entities 2. Mark entities as nonisolated if using default @MainActor 3. Wrap Core Data access in CoreDataStore 4. Use @MainActor for view context operations 5. Use @concurrent for background operations 6. Pass NSManagedObjectID between contexts 7. Test with debugging enabled
For new projects
1. Start with simple pattern (CoreDataStore) 2. Manual code generation from the start 3. Consider DAOs if heavy cross-context usage 4. Enable strict concurrency early
Common Mistakes
❌ Passing managed objects
func process(article: Article) async {
// ❌ Article not Sendable
}❌ Accessing context from wrong thread
func background() async {
let articles = viewContext.fetch(request) // ❌ Not on main thread
}❌ Using @unchecked Sendable
extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe❌ Not using perform
func save() async {
backgroundContext.save() // ❌ Not on context's thread
}Common Mistakes Agents Make
- Passing `NSManagedObject` instances across actors: Always transfer
NSManagedObjectIDor a Sendable value snapshot instead. - Using `@unchecked Sendable` on `NSManagedObject`: This does not make it thread-safe. The object is still bound to its context's queue.
- Skipping `perform { }`: All background context access must go through
performorperformAndWait. - Accessing `viewContext` from a background task: The view context belongs to the main actor; access it only from
@MainActor-isolated code.
Further Learning
For Core Data best practices, migration strategies, and advanced patterns:
Glossary
Use this when:
- You need a quick definition of a Swift Concurrency term.
- You encounter unfamiliar terminology in other reference files.
Skip this file if:
- You need implementation patterns, not definitions. Use the relevant reference file instead.
Actor isolation
A rule enforced by the compiler: actor-isolated state can only be accessed from the actor's executor. Cross-actor access requires await.
Global actor
A shared isolation domain applied via attributes like @MainActor or a custom @globalActor. Types/functions isolated to the same global actor can interact without crossing isolation.
Default actor isolation
A module/target-level setting that changes the default isolation of declarations. App targets often choose @MainActor as the default to reduce migration noise, but it changes behavior and diagnostics.
Strict concurrency checking
Compiler enforcement levels for Sendable and isolation diagnostics (minimal/targeted/complete). Raising the level typically reveals more issues and can trigger the “concurrency rabbit hole” unless migrated incrementally.
Sendable
A marker protocol that indicates a type is safe to transfer across isolation boundaries. The compiler verifies stored properties and captured values for thread-safety.
@Sendable
An annotation for function types/closures that can be executed concurrently. It tightens capture rules (captured values must be Sendable or safely transferred).
Suspension point
An await site where a task may suspend and later resume. After a suspension point, you must assume other work may have run and (for actors) state may have changed (reentrancy).
Reentrancy (actors)
While an actor is suspended at an await, other tasks can enter the actor and mutate state. Code after await must not assume actor state is unchanged.
nonisolated
Marks a declaration as not isolated to the surrounding actor/global actor. Use only when it truly does not touch isolated mutable state (typically immutable Sendable data).
nonisolated(nonsending) (Swift 6.2+ behavior)
An opt-out to prevent “sending” non-Sendable values across isolation while still allowing an async function to run in the caller’s isolation. Used to reduce Sendable friction when you do not need to hop executors.
@concurrent (Swift 6.2+ behavior)
An attribute used to explicitly opt a nonisolated async function into concurrent execution (i.e., not inheriting the caller’s actor). It is used during migration when enabling NonisolatedNonsendingByDefault. Also valid on Task { @concurrent in ... } to opt the task body out of the enclosing actor's isolation; pick this when the task's synchronous prefix (everything before the first await) does not need the main actor.
@preconcurrency
An annotation used to suppress Sendable-related diagnostics from a module that predates concurrency annotations. It reduces noise but shifts safety responsibility to you.
Region-based isolation / sending
Mechanisms that model ownership transfer so certain non-Sendable values can be moved between regions safely. The sending keyword enforces that a value is no longer used after transfer.
AsyncSequence
A protocol for types that provide asynchronous, sequential iteration over elements. Conforms to the for await loop pattern. Use for streaming data where elements arrive over time.
AsyncStream
A concrete implementation of AsyncSequence that bridges callback-based or delegate-based APIs to async/await. Provides yield() to emit values and finish() to complete the stream.
Continuation
A mechanism to bridge callback-based APIs to async/await. withCheckedContinuation and withCheckedThrowingContinuation provide safe bridging with runtime checks. withUnsafeContinuation variants skip checks for performance-critical code.
Task Local
Task-scoped storage that propagates values through the task hierarchy automatically. Declared with @TaskLocal and accessed via the wrapper's static property. Child tasks inherit parent task locals.
Cooperative thread pool
Swift's threading model where tasks run on a limited pool of threads managed by the runtime. Tasks yield cooperatively at suspension points, allowing other tasks to run. Avoid blocking operations that would starve the pool.
Executor
The scheduling mechanism that determines where and when actor code runs. MainActor uses the main thread executor. Custom actors use the default executor unless a custom executor is specified.
Structured concurrency
A pattern where child tasks have a well-defined relationship to parent tasks. Child tasks must complete before the parent scope exits. Provides automatic cancellation propagation and prevents orphaned tasks. Implemented via async let and TaskGroup.
Isolation domain
A boundary that protects mutable state from concurrent access. Each actor instance defines its own isolation domain. The @MainActor global actor defines a shared isolation domain for UI work. Code must cross isolation boundaries explicitly via await.
Task priority
A hint to the runtime about the relative importance of a task. Priorities include .high, .medium, .low, .userInitiated, .utility, and .background. Higher priority tasks are scheduled before lower priority ones. Priority can escalate when a high-priority task awaits a low-priority one.
Cancellation
A cooperative mechanism to signal that a task should stop. Check Task.isCancelled or call Task.checkCancellation() (throws) in long-running work. Cancellation propagates to child tasks in structured concurrency.
Debounce
Wait for a period of inactivity before emitting a value. Used to reduce API calls for rapid inputs like search fields. Implemented as debounce(for:tolerance:clock:) in AsyncAlgorithms.
Throttle
Emit at most one value per time interval, discarding intermediate values. Used to prevent excessive calls from repeated actions like button taps. Implemented as throttle(for:clock:reducing:) in AsyncAlgorithms.
Merge (AsyncAlgorithms)
Combine multiple asynchronous sequences into one, emitting values as they arrive from any source. Order is interleaved based on emission timing. Stable operator.
CombineLatest (AsyncAlgorithms)
Combine multiple asynchronous sequences, emitting a tuple whenever any source emits a new value. Always uses the latest value from each sequence. Stable operator.
Zip (AsyncAlgorithms)
Combine multiple asynchronous sequences by pairing elements in order. Waits for all sequences to emit before producing a tuple. Stable operator.
AsyncChannel
An AsyncSequence with backpressure sending semantics. Allows multiple producers to send values safely to multiple consumers with flow control. Stable operator.
AsyncThrowingChannel
Like AsyncChannel but can emit errors through the stream. Stable operator.
AsyncTimerSequence
An AsyncSequence that emits a value at regular intervals. Replaces timer-based publishers and manual sleep loops. Stable operator.
Linting & Concurrency
Use this when:
- SwiftLint flags
async_without_awaitor other concurrency-related warnings. - You need to decide whether to suppress, fix, or reconfigure a concurrency lint rule.
Skip this file if:
- The issue is a compiler diagnostic, not a lint rule. Use
actors.md,sendable.md, orthreading.md.
Jump to:
- SwiftLint Concurrency Rules Overview
async_without_awaitRule- Suppression Strategies
SwiftLint Concurrency Rules Overview
SwiftLint provides several rules targeting async/await and concurrency patterns. Understanding when to fix vs. suppress is critical.
| Rule | Default | Purpose |
|---|---|---|
async_without_await | warning | Flags async functions that never await |
unowned_variable_capture | warning | Warns about unowned in closures (risky in async) |
class_delegate_protocol | warning | Ensures delegates are class-bound (AnyObject) |
weak_delegate | warning | Delegates should be weak to avoid retain cycles |
SwiftLint: async_without_await
- Intent: A declaration should not be
asyncif it never awaits. - Never "fix" by inserting fake suspension (e.g.
await Task.yield(),await Task { ... }.value). Those mask the real issue and add meaningless suspension points. - Legit use of `Task.yield()`: OK in tests or scheduling control when you truly need a yield; not as a lint workaround.
Diagnose why the declaration is async
1) Protocol requirement — the protocol method/property is async. 2) Override requirement — base class API is async. 3) `@concurrent` requirement — stays async even without await. 4) Accidental/legacy `async` — no caller needs async semantics.
Preferred fixes (order)
1) Remove `async` (and adjust call sites) when no async semantics are needed. 2) If async is required (protocol/override/@concurrent):
- Re-evaluate the upstream API if you own it (can it be non-async?).
- If you cannot change it, keep
asyncand narrowly suppress the rule where appropriate (common for mocks/stubs/overrides).
Suppression examples (keep scope tight)
// swiftlint:disable:next async_without_await
func fetch() async { perform() }
// For a block:
// swiftlint:disable async_without_await
func makeMock() async { perform() }
// swiftlint:enable async_without_awaitQuick checklist
- [ ] Confirm if
asyncis truly required (protocol/override/@concurrent). - [ ] If not required, remove
asyncand update callers. - [ ] If required, prefer localized suppression over dummy awaits.
- [ ] Avoid adding new suspension points without intent.
Compiler Warnings: Sendable & Isolation
The Swift compiler generates concurrency-related warnings based on strict concurrency checking level.
Common Warning Patterns
"Capture of non-sendable type"
// Warning: Capture of 'self' with non-sendable type 'MyClass' in a `@Sendable` closure
Task {
self.doWork() // 'self' is non-Sendable
}Fixes (in order of preference): 1. Make the type Sendable if it's truly thread-safe 2. Use @MainActor isolation if it's UI-related 3. Capture only Sendable values instead of self 4. Use @unchecked Sendable with documented safety invariant (last resort)
"Non-sendable result returned"
// Warning: Non-sendable type 'MyResult' returned by implicitly async call
let result = await actor.getData() // Returns non-Sendable typeFixes: 1. Make the return type Sendable 2. Return Sendable projections (IDs, copies of data) 3. Keep processing within the actor's isolation
Actor Isolation Warnings
"Main actor-isolated property accessed from non-isolated context"
// Warning: Main actor-isolated property 'title' cannot be referenced from a non-isolated context
func updateTitle() {
viewModel.title = "New" // viewModel is @MainActor
}Fixes: 1. Mark the calling function @MainActor 2. Use await MainActor.run { } for one-off access 3. Reconsider if the property truly needs @MainActor isolation
Suppression Strategies
When to Suppress vs. Fix
Fix when:
- The warning identifies a real data race risk
- The fix is straightforward (add Sendable, adjust isolation)
- The code is new or actively maintained
Suppress when:
- Protocol/inheritance requires the signature
- Third-party code forces the pattern
- Migration is in progress (with tracked ticket)
Suppression Annotations
// Suppress Sendable warnings for legacy imports
@preconcurrency import LegacyFramework
// Suppress for a single declaration
nonisolated(unsafe) var legacyCallback: (() -> Void)?
// Type-level suppression (use sparingly)
struct LegacyWrapper: @unchecked Sendable {
// Document why this is safe
private let lock = NSLock()
private var value: Int
}Documentation Requirements
When using suppression annotations, document: 1. Why the suppression is needed 2. What invariant makes it safe 3. When it can be removed (link to migration ticket)
/// Thread-safe: Internal lock protects all mutations.
/// TODO: Remove @unchecked when migrated to actor (JIRA-1234)
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}Memory Management
Use this when:
- A task or async sequence is keeping objects alive longer than expected.
- You suspect a retain cycle between a task and its owner.
- You need to verify deallocation behavior or use
isolated deinit.
Skip this file if:
- You mainly need to protect mutable state from races. Use
actors.md. - You are debugging slow async code. Use
performance.md.
Jump to:
- Core Concepts (Task Capture)
- Retain Cycles
- One-Way Retention
- Async Sequences and Retention
- Isolated Deinit (Swift 6.2+)
- Detection and Testing
- Common Patterns
Core Concepts
Tasks capture like closures
Tasks capture variables and references just like regular closures. Swift doesn't automatically prevent retain cycles in concurrent code.
Task {
self.doWork() // ⚠️ Strong capture of self
}Why concurrency hides memory issues
- Tasks may live longer than expected
- Async operations delay execution
- Harder to track when memory should be released
- Long-running tasks can hold references indefinitely
Course Deep Dive: This topic is covered in detail in Lesson 8.1: Overview of memory management in Swift Concurrency
Retain Cycles
What is a retain cycle?
Two or more objects hold strong references to each other, preventing deallocation.
class A {
var b: B?
}
class B {
var a: A?
}
let a = A()
let b = B()
a.b = b
b.a = a // Retain cycle - neither can be deallocatedRetain cycles with Tasks
When task captures self strongly and self owns the task:
@MainActor
final class ImageLoader {
var task: Task<Void, Never>?
func startPolling() {
task = Task {
while true {
self.pollImages() // ⚠️ Strong capture
try? await Task.sleep(for: .seconds(1))
}
}
}
}
var loader: ImageLoader? = .init()
loader?.startPolling()
loader = nil // ⚠️ Loader never deallocated - retain cycle!Problem: Task holds self, self holds task → neither released.
Breaking Retain Cycles
Use weak self
func startPolling() {
task = Task { [weak self] in
while let self = self {
self.pollImages()
try? await Task.sleep(for: .seconds(1))
}
}
}
var loader: ImageLoader? = .init()
loader?.startPolling()
loader = nil // ✅ Loader deallocated, task stopsPattern for long-running tasks
task = Task { [weak self] in
while let self = self {
await self.doWork()
try? await Task.sleep(for: interval)
}
}Course Deep Dive: This topic is covered in detail in Lesson 8.2: Preventing retain cycles when using Tasks
Loop exits when self becomes nil.
One-Way Retention
Task retains self, but self doesn't retain task. Object stays alive until task completes.
@MainActor
final class ViewModel {
func fetchData() {
Task {
await performRequest()
updateUI() // ⚠️ Strong capture
}
}
}
var viewModel: ViewModel? = .init()
viewModel?.fetchData()
viewModel = nil // ViewModel stays alive until task completesExecution order: 1. Task starts 2. viewModel = nil (but object not deallocated) 3. Task completes 4. ViewModel finally deallocated
When one-way retention is acceptable
Short-lived tasks that complete quickly:
func saveData() {
Task {
await database.save(self.data) // OK - completes quickly
}
}When to use weak self
Long-running or indefinite tasks:
func startMonitoring() {
Task { [weak self] in
for await event in eventStream {
self?.handle(event)
}
}
}Async Sequences and Retention
Problem: Infinite sequences
@MainActor
final class AppLifecycleViewModel {
private(set) var isActive = false
private var task: Task<Void, Never>?
func startObserving() {
task = Task {
for await _ in NotificationCenter.default.notifications(
named: .didBecomeActive
) {
isActive = true // ⚠️ Strong capture, never ends
}
}
}
}
var viewModel: AppLifecycleViewModel? = .init()
viewModel?.startObserving()
viewModel = nil // ⚠️ Never deallocated - sequence continuesProblem: Async sequence never finishes, task holds self indefinitely.
Solution 1: Manual cancellation
func startObserving() {
task = Task {
for await _ in NotificationCenter.default.notifications(
named: .didBecomeActive
) {
isActive = true
}
}
}
func stopObserving() {
task?.cancel()
}
// Usage
viewModel?.startObserving()
viewModel?.stopObserving() // Must call before release
viewModel = nilSolution 2: Weak self with guard
func startObserving() {
task = Task { [weak self] in
for await _ in NotificationCenter.default.notifications(
named: .didBecomeActive
) {
guard let self = self else { return }
self.isActive = true
}
}
}Task exits when self deallocates.
Isolated deinit (Swift 6.2+)
Clean up actor-isolated state in deinit:
@MainActor
final class ViewModel {
private var task: Task<Void, Never>?
isolated deinit {
task?.cancel()
}
}Limitation: Won't break retain cycles (deinit never called if cycle exists).
Use for: Cleanup when object is being deallocated normally.
Common Patterns
Short-lived task (strong capture OK)
func saveData() {
Task {
await database.save(self.data)
self.updateUI()
}
}When safe: Task completes quickly, acceptable for object to live until done.
Long-running task (weak self required)
func startPolling() {
task = Task { [weak self] in
while let self = self {
await self.fetchUpdates()
try? await Task.sleep(for: .seconds(5))
}
}
}Async sequence monitoring (weak self + guard)
func startMonitoring() {
task = Task { [weak self] in
for await event in eventStream {
guard let self = self else { return }
self.handle(event)
}
}
}Cancellable work with cleanup
func startWork() {
task = Task { [weak self] in
defer { self?.cleanup() }
while let self = self {
await self.doWork()
try? await Task.sleep(for: .seconds(1))
}
}
}Detection Strategies
Add deinit logging
deinit {
print("✅ \(type(of: self)) deallocated")
}If deinit never prints → likely retain cycle.
Memory graph debugger
1. Run app in Xcode 2. Debug → Debug Memory Graph 3. Look for cycles in object graph
Instruments
Use Leaks instrument to detect retain cycles at runtime.
Decision Tree
Task captures self?
├─ Task completes quickly?
│ └─ Strong capture OK
│
├─ Long-running or infinite?
│ ├─ Can use weak self? → Use [weak self]
│ ├─ Need manual control? → Store task, cancel explicitly
│ └─ Async sequence? → [weak self] + guard
│
└─ Self owns task?
├─ Yes → High risk of retain cycle
└─ No → Lower risk, but check lifetimeBest Practices
1. Default to weak self for long-running tasks 2. Use guard let self in async sequences 3. Cancel tasks explicitly when possible 4. Add deinit logging during development 5. Test object deallocation in unit tests 6. Use Memory Graph to verify no cycles 7. Document lifetime expectations in comments 8. Prefer cancellation over weak self when possible 9. Avoid nested strong captures in task closures 10. Use isolated deinit for cleanup (Swift 6.2+)
Testing for Leaks
Unit test pattern
func testViewModelDeallocates() async {
var viewModel: ViewModel? = ViewModel()
weak var weakViewModel = viewModel
viewModel?.startWork()
viewModel = nil
// Give tasks time to complete
try? await Task.sleep(for: .milliseconds(100))
XCTAssertNil(weakViewModel, "ViewModel should be deallocated")
}SwiftUI view test
func testViewDeallocates() {
var view: MyView? = MyView()
weak var weakView = view
view = nil
XCTAssertNil(weakView)
}Common Mistakes
❌ Forgetting weak self in loops
Task {
while true {
self.poll() // Retain cycle
try? await Task.sleep(for: .seconds(1))
}
}❌ Strong capture in async sequences
Task {
for await item in stream {
self.process(item) // May never release
}
}❌ Not canceling stored tasks
class Manager {
var task: Task<Void, Never>?
func start() {
task = Task {
await self.work() // Retain cycle
}
}
// Missing: deinit { task?.cancel() }
}❌ Assuming deinit breaks cycles
deinit {
task?.cancel() // Never called if retain cycle exists
}Examples by Use Case
Polling service
final class PollingService {
private var task: Task<Void, Never>?
func start() {
task = Task { [weak self] in
while let self = self {
await self.poll()
try? await Task.sleep(for: .seconds(5))
}
}
}
func stop() {
task?.cancel()
}
}Notification observer
@MainActor
final class NotificationObserver {
private var task: Task<Void, Never>?
func startObserving() {
task = Task { [weak self] in
for await notification in NotificationCenter.default.notifications(
named: .someNotification
) {
guard let self = self else { return }
self.handle(notification)
}
}
}
isolated deinit {
task?.cancel()
}
}Download manager
final class DownloadManager {
private var tasks: [URL: Task<Data, Error>] = [:]
func download(_ url: URL) async throws -> Data {
let task = Task { [weak self] in
defer { self?.tasks.removeValue(forKey: url) }
return try await URLSession.shared.data(from: url).0
}
tasks[url] = task
return try await task.value
}
func cancelAll() {
tasks.values.forEach { $0.cancel() }
tasks.removeAll()
}
}Timer
actor Timer {
private var task: Task<Void, Never>?
func start(interval: Duration, action: @Sendable () async -> Void) {
task = Task {
while !Task.isCancelled {
await action()
try? await Task.sleep(for: interval)
}
}
}
func stop() {
task?.cancel()
}
}Common Mistakes Agents Make
- Forgetting `[weak self]` in stored tasks: When
selfowns the task and the task capturesself, a retain cycle prevents deallocation. - Strong capture in infinite `AsyncSequence` loops:
for awaitover an infinite sequence with a strongselfcapture keeps the object alive forever. - Not cancelling stored tasks on cleanup: If the task outlives its owner, it retains captured objects indefinitely.
- Assuming `isolated deinit` breaks retain cycles:
isolated deinitruns cleanup on the correct actor, but if a cycle preventsdeinitfrom being called at all, the cleanup never executes. - Using `try?` in loops with `Task.sleep`:
try?can swallowCancellationError, causing the loop to continue running after cancellation. Always checkTask.isCancelledexplicitly.
Debugging Checklist
When object won't deallocate:
- [ ] Check for strong self captures in tasks
- [ ] Verify tasks are canceled or complete
- [ ] Look for infinite loops or sequences
- [ ] Check if self owns the task
- [ ] Use Memory Graph to find cycles
- [ ] Add deinit logging to verify
- [ ] Test with weak references
- [ ] Review async sequence usage
- [ ] Check nested task captures
- [ ] Verify cleanup in deinit
Further Learning
For migration strategies, real-world examples, and advanced memory patterns, see Swift Concurrency Course.
Related skills
How it compares
Use swift-concurrency for in-session Swift async/agent guidance; consult Apple WWDC docs for platform release notes outside agent workflows.
FAQ
What does swift-concurrency do?
Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compile
When should I use swift-concurrency?
Diagnose Swift Concurrency issues, refactor callback-based code to async/await, and guide Swift 6 migration when working with tasks, actors, @MainActor, Sendable, data races, thread safety, or concurrency-related compile
Is swift-concurrency safe to install?
Review the Security Audits panel on this page before installing in production.