references/_index.md
# 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`
references/actors.md
# Actors
Use this when:
- You need to protect class-based mutable state from concurrent access.
- You are choosing between `actor`, `@MainActor`, `nonisolated`, or `Mutex`.
- 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+)
- `#isolation` Macro
- 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.
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Actor Isolation
### Enforced by compiler
```swift
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 methods
```
### Reading properties
```swift
let account = BankAccount()
await account.deposit(100)
print(await account.balance) // Must await reads too
```
Always 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 `NSObject` for Objective-C interop)
- **Automatic isolation** (no manual locks needed)
- **Implicit Sendable** conformance
```swift
// ❌ Can't inherit from actors
actor Base {}
actor Child: Base {} // Error
// ✅ NSObject exception
actor Example: NSObject {} // OK for Objective-C
```
## Global Actors
Shared isolation domain across types, functions, and properties.
### @MainActor
Ensures execution on main thread:
```swift
@MainActor
final class ViewModel {
var items: [Item] = []
}
@MainActor
func updateUI() {
// Always runs on main thread
}
@MainActor
var title: String = ""
```
### Custom global actors
```swift
@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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## @MainActor Best Practices
### When to use
UI-related code that must run on main thread:
```swift
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
}
```
### Replacing DispatchQueue.main
```swift
// 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:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Isolated vs Nonisolated
### Default: Isolated
Actor methods are isolated by default:
```swift
actor BankAccount {
var balance: Double
// Implicitly isolated
func deposit(_ amount: Double) {
balance += amount
}
}
```
### Isolated parameters
Reduce suspension points by inheriting caller's isolation:
```swift
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
```swift
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
```swift
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:
```swift
actor BankAccount {
let accountHolder: String
nonisolated var details: String {
"Account: \(accountHolder)"
}
}
// No await needed
print(account.details)
```
### Protocol conformance
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Isolated Deinit (Swift 6.2+)
Clean up actor state on deallocation:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Global Actor Isolated Conformance (Swift 6.2+)
Protocol conformance respecting actor isolation:
```swift
@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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### `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`.
```swift
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:
```swift
@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.
```swift
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
```swift
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: 150
```
### Solution
Complete actor work before suspending:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## #isolation Macro
Inherit caller's isolation for generic code:
```swift
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:
```swift
func process(delegate: NonSendableDelegate) {
Task {
delegate.doWork() // ❌ Error: capturing non-Sendable type
}
}
```
**Solution**: Use `#isolation` parameter and capture it inside the `Task`:
```swift
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](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md), `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 `Task`s that work with `non-Sendable` delegate 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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Custom Actor Executors
**Advanced**: Control how actor schedules work.
### Serial executor
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Mutex: Alternative to Actors
Synchronous locking without async/await overhead (iOS 18+, macOS 15+).
### Basic usage
```swift
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
```swift
final class TouchesCapturer: Sendable {
let path = Mutex<NSBezierPath>(NSBezierPath())
func storeTouch(_ point: NSPoint) {
path.withLock { path in
path.move(to: point)
}
}
}
```
### Error handling
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Common Patterns
### View model with @MainActor
```swift
@MainActor
final class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
func loadItems() async {
items = try await api.fetchItems()
}
}
```
### Background processing with custom actor
```swift
@ImageProcessing
final class ImageProcessor {
func process(_ images: [UIImage]) async -> [UIImage] {
images.map { applyFilters($0) }
}
}
```
### Mixed isolation
```swift
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
```swift
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? → Mutex
```
## Further Learning
For migration strategies, advanced patterns, and real-world examples, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).
references/async-algorithms.md
# 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 `AsyncStream` bridging for callbacks or delegates. Use `async-sequences.md`.
- You are choosing between `Task`, `async let`, or task groups. Use `tasks.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:
```swift
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](https://github.com/apple/swift-async-algorithms)
---
## 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
```swift
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:
```swift
import AsyncAlgorithms
```
---
## Time-Based Operators
### debounce(for:tolerance:clock:)
Wait for inactivity before emitting. Use for rapid inputs like search fields.
#### Example: ArticleSearcher
```swift
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
```swift
// 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
```swift
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
```swift
// .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
```swift
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
```swift
// 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
```swift
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
```swift
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
```swift
// 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
```swift
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
```swift
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 ✅**
```swift
import AsyncAlgorithms
actor ChatHistory {
private var messageStream = AsyncStream<ChatMessage> { /* ... */ }
func getUniqueMessages() -> AsyncStream<ChatMessage> {
messageStream.removeDuplicates()
}
}
```
---
### chunks() and chunked()
Collect values into batches. **Stable operator ✅**
```swift
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()
```swift
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
```swift
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
```swift
// 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
```swift
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**
```swift
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**
```swift
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**
```swift
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**
```swift
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**
```swift
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**
```swift
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 change
- `async 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 `AsyncChannel` for multi-consumer scenarios with backpressure. Note: `AsyncChannel` is point-to-point, not broadcast like Combine's `.share()`.
- **Looking for a `.flatMap` equivalent**: Use `TaskGroup` for fan-out; the semantics differ from Combine/Rx `flatMap`.
- **Looking for `.receive(on:)` equivalent**: Use `@MainActor` or `Task` context 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](https://github.com/apple/swift-async-algorithms)
- [Combine Migration Guide](migration.md)
- [Async Sequences](async-sequences.md)
- [Tasks](tasks.md) - Task groups and structured concurrency
references/async-await-basics.md
# 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`. Use `tasks.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:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Calling Async Functions
### From synchronous context
Use `Task` to bridge from sync to async:
```swift
Task {
let data = try await fetchData()
}
```
### From async context
Use `await` directly:
```swift
func processData() async throws {
let data = try await fetchData()
// process data
}
```
## Execution Order
Structured concurrency executes top-to-bottom in the order you expect:
```swift
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 completes
```
Code 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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Parallel Execution with async let
Use `async let` to run multiple operations concurrently:
```swift
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 await` in the `async let` line itself
```swift
// Redundant - avoid this
async let data = try await fetchData()
// Correct - errors handled at await point
async let data = fetchData()
let result = try await data
```
### When 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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## URLSession with Async/Await
URLSession provides async alternatives to closure-based APIs:
```swift
// 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 `data` or `response` to unwrap
- Automatic error throwing
- Compiler enforces return values
- Simpler error handling with do-catch
### Complete network request pattern
```swift
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
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Typed Errors (Swift 6)
Specify exact error types for better API contracts:
```swift
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)
```swift
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)
```swift
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
```swift
// 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](https://www.swiftconcurrencycourse.com).
references/async-sequences.md
# 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. Use `tasks.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
```swift
for await value in someAsyncSequence {
print(value)
}
```
**Key difference from Sequence**: Values may not all be available immediately.
### Custom implementation
```swift
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:
```swift
// 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) // true
```
### Termination
Return `nil` from `next()` to end iteration:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## AsyncStream
Convenient way to create async sequences without implementing protocols.
### Basic creation
```swift
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:
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Bridging Closures to Streams
### Progress + completion handlers
```swift
// 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
```swift
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
```swift
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
```swift
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
```swift
let task = Task {
for try await status in download(url) {
print(status)
}
}
task.cancel() // Triggers onTermination with .cancelled
```
**No 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:
```swift
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:
```swift
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:
```swift
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:
```swift
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:
```swift
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
```swift
let stream = NotificationCenter.default.notifications(
named: .NSSystemTimeZoneDidChange
)
for await notification in stream {
print("Time zone changed")
}
```
### Combine publishers
```swift
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
```swift
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:
```swift
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: 3
```
**Solution**: Create separate streams or use third-party libraries (AsyncExtensions).
### No values after termination
Once finished, stream won't emit new values:
```swift
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](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
```swift
// 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
```swift
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
```swift
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
```swift
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
```swift
continuation.onTermination = { reason in
print("Stream ended: \(reason)")
}
```
### Validate finish() calls
```swift
// ❌ 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
```swift
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
```swift
// ❌ 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 `AsyncChannel` for 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](https://www.swiftconcurrencycourse.com).
references/core-data.md
# Core Data and Swift Concurrency
Use this when:
- You need to use Core Data with async/await or actors.
- `NSManagedObject` instances are crossing context or actor boundaries.
- You are resolving default `@MainActor` isolation 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 `NSManagedObject` between threads
- Must access objects on their context's thread
- `NSManagedObjectID` is thread-safe (can pass around)
### NSManagedObject cannot be Sendable
```swift
@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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Available Async APIs
### Context perform
```swift
extension NSManagedObjectContext {
func perform<T>(
schedule: ScheduledTaskType = .immediate,
_ block: @escaping () throws -> T
) async rethrows -> T
}
```
### What's missing
No async alternative for:
```swift
func loadPersistentStores(
completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void
)
```
Must bridge manually (see below).
## Data Access Objects (DAO)
Thread-safe value types representing managed objects.
### Pattern
```swift
// 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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Working Without DAOs
Pass only `NSManagedObjectID` between contexts.
### Basic pattern
```swift
@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
```swift
// Safe to pass between tasks
let articleID = article.objectID
Task {
await processInBackground(articleID: articleID)
}
```
## Bridging Closures to Async
### Load persistent stores
```swift
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:
```swift
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
```swift
// 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)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Implementation
```swift
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
```swift
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:
```swift
// 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`:
```swift
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
```swift
@MainActor
func fetchArticles() throws -> [Article] {
let request = Article.fetchRequest()
return try viewContext.fetch(request)
}
```
### Background save
```swift
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
```swift
@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
```swift
@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
```swift
@main
struct MyApp: App {
let persistentContainer = NSPersistentContainer(name: "MyApp")
var body: some Scene {
WindowGroup {
ContentView()
.environment(\.managedObjectContext, persistentContainer.viewContext)
}
}
}
```
### View usage
```swift
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
```swift
// Launch argument
-com.apple.CoreData.ConcurrencyDebug 1
```
Crashes immediately on thread violations.
### Thread Sanitizer
Enable in scheme settings to catch data races.
### Assertions
```swift
@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 NSManagedObjectID
```
## Migration 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
```swift
func process(article: Article) async {
// ❌ Article not Sendable
}
```
### ❌ Accessing context from wrong thread
```swift
func background() async {
let articles = viewContext.fetch(request) // ❌ Not on main thread
}
```
### ❌ Using @unchecked Sendable
```swift
extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe
```
### ❌ Not using perform
```swift
func save() async {
backgroundContext.save() // ❌ Not on context's thread
}
```
## Common Mistakes Agents Make
- **Passing `NSManagedObject` instances across actors**: Always transfer `NSManagedObjectID` or 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 `perform` or `performAndWait`.
- **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:
- [Core Data Best Practices](https://github.com/avanderlee/CoreDataBestPractices)
- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)
references/glossary.md
# 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.
references/linting.md
# Linting & Concurrency
Use this when:
- SwiftLint flags `async_without_await` or 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`, or `threading.md`.
Jump to:
- SwiftLint Concurrency Rules Overview
- `async_without_await` Rule
- 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 `async` if 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 `async` and **narrowly suppress the rule** where appropriate (common for mocks/stubs/overrides).
### Suppression examples (keep scope tight)
```swift
// 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_await
```
### Quick checklist
- [ ] Confirm if `async` is truly required (protocol/override/@concurrent).
- [ ] If not required, remove `async` and 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"**
```swift
// 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"**
```swift
// Warning: Non-sendable type 'MyResult' returned by implicitly async call
let result = await actor.getData() // Returns non-Sendable type
```
**Fixes:**
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"**
```swift
// 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
```swift
// 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)
```swift
/// 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] = [:]
}
```
references/memory-management.md
# 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.
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Retain Cycles
### What is a retain cycle?
Two or more objects hold strong references to each other, preventing deallocation.
```swift
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 deallocated
```
### Retain cycles with Tasks
When task captures `self` strongly and `self` owns the task:
```swift
@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
```swift
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 stops
```
### Pattern for long-running tasks
```swift
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](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
Loop exits when `self` becomes `nil`.
## One-Way Retention
Task retains `self`, but `self` doesn't retain task. Object stays alive until task completes.
```swift
@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 completes
```
**Execution 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:
```swift
func saveData() {
Task {
await database.save(self.data) // OK - completes quickly
}
}
```
### When to use weak self
Long-running or indefinite tasks:
```swift
func startMonitoring() {
Task { [weak self] in
for await event in eventStream {
self?.handle(event)
}
}
}
```
## Async Sequences and Retention
### Problem: Infinite sequences
```swift
@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 continues
```
**Problem**: Async sequence never finishes, task holds `self` indefinitely.
### Solution 1: Manual cancellation
```swift
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 = nil
```
### Solution 2: Weak self with guard
```swift
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:
```swift
@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)
```swift
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)
```swift
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)
```swift
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
```swift
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
```swift
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 lifetime
```
## Best 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
```swift
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
```swift
func testViewDeallocates() {
var view: MyView? = MyView()
weak var weakView = view
view = nil
XCTAssertNil(weakView)
}
```
## Common Mistakes
### ❌ Forgetting weak self in loops
```swift
Task {
while true {
self.poll() // Retain cycle
try? await Task.sleep(for: .seconds(1))
}
}
```
### ❌ Strong capture in async sequences
```swift
Task {
for await item in stream {
self.process(item) // May never release
}
}
```
### ❌ Not canceling stored tasks
```swift
class Manager {
var task: Task<Void, Never>?
func start() {
task = Task {
await self.work() // Retain cycle
}
}
// Missing: deinit { task?.cancel() }
}
```
### ❌ Assuming deinit breaks cycles
```swift
deinit {
task?.cancel() // Never called if retain cycle exists
}
```
## Examples by Use Case
### Polling service
```swift
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
```swift
@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
```swift
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
```swift
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 `self` owns the task and the task captures `self`, a retain cycle prevents deallocation.
- **Strong capture in infinite `AsyncSequence` loops**: `for await` over an infinite sequence with a strong `self` capture 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 deinit` runs cleanup on the correct actor, but if a cycle prevents `deinit` from being called at all, the cleanup never executes.
- **Using `try?` in loops with `Task.sleep`**: `try?` can swallow `CancellationError`, causing the loop to continue running after cancellation. Always check `Task.isCancelled` explicitly.
## 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](https://www.swiftconcurrencycourse.com).
references/migration.md
# Migration to Swift 6 and Strict Concurrency
Use this when:
- You are moving an existing codebase toward Swift 6 or stricter concurrency checking.
- Compiler diagnostics depend on language mode, default isolation, or upcoming features.
- You need the smallest safe migration sequence instead of a full architectural rewrite.
Skip this file if:
- You already know the exact diagnostic and only need a local fix. Start from `actors.md`, `sendable.md`, or `threading.md`.
- You are looking for debounce, stream composition, or FRP operator replacements. Use `async-algorithms.md`.
Jump to:
- Project Settings
- Six Migration Habits
- Step-by-Step Migration
- Migration Tooling
- Rewriting Closures to Async/Await
- Migrating from Combine/RxSwift
- Concurrency-Safe Notifications (iOS 26+)
- Anti-Patterns
---
## Why Migrate to Swift 6?
Swift 6 doesn't fundamentally change how Swift Concurrency works—it **enforces existing rules more strictly**:
- **Compile-time safety**: Catches data races and threading issues at compile time instead of runtime
- **Warnings become errors**: Many Swift 5 warnings become hard errors in Swift 6 language mode
- **Future-proofing**: New concurrency features will build on this stricter foundation
- **Better maintainability**: Code becomes safer and easier to reason about
> **Important**: You can adopt strict concurrency checking gradually while still compiling under Swift 5. You don't need to flip the Swift 6 switch immediately.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.2: The impact of Swift 6 on Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Project Settings That Change Concurrency Behavior
Before interpreting diagnostics or choosing a fix, confirm the target/module settings. These settings can materially change how code executes and what the compiler enforces.
### Quick matrix
| Setting / feature | Where to check | Why it matters |
|---|---|---|
| Swift language mode (Swift 5.x vs Swift 6) | Xcode build settings (`SWIFT_VERSION`) / SwiftPM `// swift-tools-version:` | Swift 6 turns many warnings into errors and enables stricter defaults. |
| Strict concurrency checking | Xcode: Strict Concurrency Checking (`SWIFT_STRICT_CONCURRENCY`) / SwiftPM: strict concurrency flags | Controls how aggressively Sendable + isolation rules are enforced. |
| Default actor isolation | Xcode: Default Actor Isolation (`SWIFT_DEFAULT_ACTOR_ISOLATION`) / SwiftPM: `.defaultIsolation(MainActor.self)` | Changes the default isolation of declarations; can reduce migration noise but changes behavior and requirements. |
| `NonisolatedNonsendingByDefault` | Xcode upcoming feature / SwiftPM `.enableUpcomingFeature("NonisolatedNonsendingByDefault")` | Changes how nonisolated async functions execute (can inherit the caller’s actor unless explicitly marked `@concurrent`). |
| Approachable Concurrency | Xcode build setting / SwiftPM enables the underlying upcoming features | Bundles multiple upcoming features; recommended to migrate feature-by-feature first. |
## The Concurrency Rabbit Hole
A common migration experience:
1. Enable strict concurrency checking
2. See 50+ errors and warnings
3. Fix a bunch of them
4. Rebuild and see 80+ new errors appear
**Why this happens**: Fixing isolation in one place often exposes issues elsewhere. This is normal and manageable with the right strategy.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.1: Challenges in migrating to Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Six Migration Habits for Success
### 1. Don't Panic—It's All About Iterations
Break migration into small, manageable chunks:
```swift
// Day 1: Enable strict concurrency, fix a few warnings
// Build Settings → Strict Concurrency Checking = Complete
// Day 2: Fix more warnings
// Day 3: Revert to minimal checking if needed
// Build Settings → Strict Concurrency Checking = Minimal
```
Allow yourself 30 minutes per day to migrate gradually. Don't expect completion in a few days for large projects.
### 2. Sendable by Default for New Code
When writing new types, make them `Sendable` from the start:
```swift
// ✅ Good: New code prepared for Swift 6
struct UserProfile: Sendable {
let id: UUID
let name: String
}
// ❌ Avoid: Creating technical debt
class UserProfile { // Will need migration later
var id: UUID
var name: String
}
```
It's easier to design for concurrency upfront than to retrofit it later.
### 3. Use Swift 6 for New Projects and Packages
For new projects, packages, or files:
- Enable Swift 6 language mode from the start
- Use Swift Concurrency features (async/await, actors)
- Reduce technical debt before it accumulates
You can enable Swift 6 for individual files in a Swift 5 project to prevent scope creep.
### 4. Resist the Urge to Refactor
**Focus solely on concurrency changes**. Don't combine migration with:
- Architecture refactors
- API modernization
- Code style improvements
Create separate tickets for non-concurrency refactors and address them later.
### 5. Focus on Minimal Changes
- Make small, focused pull requests
- Migrate one class or module at a time
- Get changes merged quickly to create checkpoints
- Avoid large PRs that are hard to review
### 6. Don't Just @MainActor All the Things
Don't blindly add `@MainActor` to fix warnings. Consider:
- Should this actually run on the main actor?
- Would a custom actor be more appropriate?
- Is `nonisolated` the right choice?
**Exception**: For app projects (not frameworks), consider enabling **Default Actor Isolation** to `@MainActor`, since most app code needs main thread access.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.3: The six migration habits for a successful migration](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Step-by-Step Migration Process
### 1. Find an Isolated Piece of Code
Start with:
- Standalone packages with minimal dependencies
- Individual Swift files within a package
- Code that's not heavily used throughout the project
**Why**: Fewer dependencies = less risk of falling into the concurrency rabbit hole.
### 2. Update Related Dependencies
Before enabling strict concurrency:
```swift
// Update third-party packages to latest versions
// Example: Vapor, Alamofire, etc.
```
Apply these updates in a separate PR before proceeding with concurrency changes.
### 3. Add Async Alternatives
Provide async/await wrappers for existing closure-based APIs:
```swift
// Original closure-based API
@available(*, deprecated, renamed: "fetchImage(urlRequest:)",
message: "Consider using the async/await alternative.")
func fetchImage(urlRequest: URLRequest,
completion: @escaping @Sendable (Result<UIImage, Error>) -> Void) {
// ... existing implementation
}
// New async wrapper
func fetchImage(urlRequest: URLRequest) async throws -> UIImage {
return try await withCheckedThrowingContinuation { continuation in
fetchImage(urlRequest: urlRequest) { result in
continuation.resume(with: result)
}
}
}
```
**Benefits**:
- Colleagues can start using async/await immediately
- You can migrate callers before rewriting implementation
- Tests can be updated to async/await first
**Tip**: Use Xcode's **Refactor → Add Async Wrapper** to generate these automatically.
### 4. Change Default Actor Isolation (Swift 6.2+)
For app projects, set default isolation to `@MainActor`:
**Xcode Build Settings**:
```
Swift Concurrency → Default Actor Isolation = MainActor
```
**Swift Package Manager**:
```swift
.target(
name: "MyTarget",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
```
This drastically reduces warnings in app code where most types need main thread access.
### 5. Enable Strict Concurrency Checking
**Xcode Build Settings**: Search for "Strict Concurrency Checking"
Three levels available:
- **Minimal**: Only checks code that explicitly adopts concurrency (`@Sendable`, `@MainActor`)
- **Targeted**: Checks all code that adopts concurrency, including `Sendable` conformances
- **Complete**: Checks entire codebase (matches Swift 6 behavior)
**Swift Package Manager**:
```swift
.target(
name: "MyTarget",
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency=targeted")
]
)
```
**Strategy**: Start with Minimal → Targeted → Complete, fixing errors at each level.
### 6. Add Sendable Conformances
Even if the compiler doesn't complain, add `Sendable` to types that will cross isolation domains:
```swift
// ✅ Prepare for future use
struct Configuration: Sendable {
let apiKey: String
let timeout: TimeInterval
}
```
This prevents warnings when the type is used in concurrent contexts later.
### 7. Enable Approachable Concurrency (Swift 6.2+)
**Xcode Build Settings**: Search for "Approachable Concurrency"
Enables multiple upcoming features at once:
- `DisableOutwardActorInference`
- `GlobalActorIsolatedTypesUsability`
- `InferIsolatedConformances`
- `InferSendableFromCaptures`
- `NonisolatedNonsendingByDefault`
**⚠️ Warning**: Don't just flip this switch for existing projects. Use migration tooling (see below) to migrate to each feature individually first.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.5: The Approachable Concurrency build setting (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### 8. Enable Upcoming Features
**Xcode Build Settings**: Search for "Upcoming Feature"
Enable features individually:
**Swift Package Manager**:
```swift
.target(
name: "MyTarget",
swiftSettings: [
.enableUpcomingFeature("ExistentialAny"),
.enableUpcomingFeature("InferIsolatedConformances")
]
)
```
Find feature keys in Swift Evolution proposals (e.g., SE-335 for `ExistentialAny`).
### 9. Change to Swift 6 Language Mode
**Xcode Build Settings**:
```
Swift Language Version = Swift 6
```
**Swift Package Manager**:
```swift
// swift-tools-version: 6.0
```
If you've completed all previous steps, you should have minimal new errors.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.4: Steps to migrate existing code to Swift 6 and Strict Concurrency Checking](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Migration Tooling for Upcoming Features
Swift 6.2+ includes **semi-automatic migration** for upcoming features.
### Xcode Migration
1. Go to Build Settings → Find the upcoming feature (e.g., "Require Existential any")
2. Set to **Migrate** (temporary setting)
3. Build the project
4. Warnings appear with **Apply** buttons
5. Click Apply for each warning
**Example warning**:
```swift
// ⚠️ Use of protocol 'Error' as a type must be written 'any Error'
func fetchData() throws -> Data // Before
func fetchData() throws -> any Data // After applying fix
```
### Package Migration
Use the `swift package migrate` command:
```bash
# Migrate all targets
swift package migrate --to-feature ExistentialAny
# Migrate specific target
swift package migrate --target MyTarget --to-feature ExistentialAny
```
**Output**:
```
> Applied 24 fix-its in 11 files (0.016s)
> Updating manifest
```
The tool automatically:
- Applies all fix-its
- Updates `Package.swift` to enable the feature
**Available migrations** (as of Swift 6.2):
- `ExistentialAny` (SE-335)
- `InferIsolatedConformances` (SE-470)
- More features will add migration support over time
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.6: Migration tooling for upcoming Swift features](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
**Additional resource**: [Migration Tooling Video](https://youtu.be/FK9XFxSWZPg?si=2z_ybn1t1YCJow5k)
---
## Rewriting Closures to Async/Await
### Using Xcode Refactoring
Three refactoring options available:
1. **Add Async Wrapper**: Wraps existing closure-based method (recommended first step)
2. **Add Async Alternative**: Rewrites method as async, keeps original
3. **Convert Function to Async**: Replaces method entirely
**⚠️ Known Issue**: Refactoring can be unstable in Xcode. If you get "Connection interrupted" errors:
- Clean build folder
- Clear derived data
- Restart Xcode
- Simplify complex methods (shorthand if statements can cause failures)
### Manual Rewriting Example
**Before** (closure-based):
```swift
func fetchImage(urlRequest: URLRequest,
completion: @escaping @Sendable (Result<UIImage, Error>) -> Void) {
URLSession.shared.dataTask(with: urlRequest) { data, _, error in
do {
if let error = error { throw error }
guard let data = data, let image = UIImage(data: data) else {
throw ImageError.conversionFailed
}
completion(.success(image))
} catch {
completion(.failure(error))
}
}.resume()
}
```
**After** (async/await):
```swift
func fetchImage(urlRequest: URLRequest) async throws -> UIImage {
let (data, _) = try await URLSession.shared.data(for: urlRequest)
guard let image = UIImage(data: data) else {
throw ImageError.conversionFailed
}
return image
}
```
**Benefits**:
- Less code to maintain
- Easier to reason about
- No nested closures
- Automatic error propagation
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.7: Techniques for rewriting closures to async/await syntax](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Using @preconcurrency
Suppresses `Sendable` warnings from modules you don't control.
### When to Use
```swift
// ⚠️ Third-party library doesn't support Swift Concurrency yet
@preconcurrency import SomeThirdPartyLibrary
actor DataProcessor {
func process(_ data: LibraryType) { // No Sendable warning
// ...
}
}
```
### Risks
- **No compile-time safety**: You're responsible for ensuring thread safety
- **Hides real issues**: Library might not be thread-safe at all
- **Technical debt**: Easy to forget to revisit later
### Best Practices
1. **Don't use by default**: Only add when compiler suggests it
2. **Check for updates first**: Library might have a newer version with concurrency support
3. **Document why**: Add a comment explaining why it's needed
4. **Revisit regularly**: Set reminders to check if library has been updated
```swift
// TODO: Remove @preconcurrency when SomeLibrary adds Sendable support
// Last checked: 2026-01-07 (version 2.3.0)
@preconcurrency import SomeLibrary
```
The compiler will warn if `@preconcurrency` is unused:
```
'@preconcurrency' attribute on module 'SomeModule' is unused
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.8: How and when to use @preconcurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Migrating from Combine/RxSwift
### Observation Alternative
Swift 6 will include **Transactional Observation** (SE-475):
```swift
// Future API (not yet implemented)
let names = Observations { person.name }
Task.detached {
for await name in names {
print("Name updated to: \(name)")
}
}
```
**Current alternatives**:
- Use `@Observable` macro for SwiftUI
- Use `AsyncStream` for custom observation
- Consider [AsyncExtensions](https://github.com/sideeffect-io/AsyncExtensions) package
### Debouncing Example
**Combine**:
```swift
$searchQuery
.debounce(for: .milliseconds(500), scheduler: DispatchQueue.main)
.sink { [weak self] query in
self?.performSearch(query)
}
.store(in: &cancellables)
```
**Swift Concurrency**:
```swift
func search(_ query: String) {
currentSearchTask?.cancel()
currentSearchTask = Task {
do {
try await Task.sleep(for: .milliseconds(500))
performSearch(query)
} catch {
// Search was cancelled
}
}
}
```
**SwiftUI Integration**:
```swift
struct SearchView: View {
@State private var searchQuery = ""
@State private var searcher = ArticleSearcher()
var body: some View {
List(searcher.results) { result in
Text(result.title)
}
.searchable(text: $searchQuery)
.onChange(of: searchQuery) { _, newValue in
searcher.search(newValue)
}
}
}
```
### Mindset Shift
**Don't think in Combine pipelines**. Many problems are simpler without FRP:
```swift
// ❌ Looking for AsyncSequence equivalent of complex Combine pipeline
somePublisher
.debounce(for: .seconds(0.5))
.removeDuplicates()
.flatMap { ... }
.sink { ... }
// ✅ Rethink the problem with Swift Concurrency
Task {
var lastValue: String?
for await value in stream {
guard value != lastValue else { continue }
lastValue = value
try await Task.sleep(for: .seconds(0.5))
await process(value)
}
}
```
**For complex operators**: Check [Swift Async Algorithms](https://github.com/apple/swift-async-algorithms) package.
### ⚠️ Critical: Actor Isolation with Combine
**Problem**: `sink` closures don't respect actor isolation at compile time.
```swift
@MainActor
final class NotificationObserver {
private var cancellables: [AnyCancellable] = []
init() {
NotificationCenter.default.publisher(for: .someNotification)
.sink { [weak self] _ in
self?.handleNotification() // ⚠️ May crash if posted from background
}
.store(in: &cancellables)
}
private func handleNotification() {
// Expects to run on main actor
}
}
```
**Why it crashes**: Notification observers run on the same thread as the poster. If posted from a background thread, the `@MainActor` method is called off the main actor.
**Solutions**:
1. **Migrate to Swift Concurrency** (recommended):
```swift
Task { [weak self] in
for await _ in NotificationCenter.default.notifications(named: .someNotification) {
await self?.handleNotification() // ✅ Compile-time safe
}
}
```
2. **Use Task wrapper** (temporary):
```swift
.sink { [weak self] _ in
Task { @MainActor in
self?.handleNotification()
}
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.9: Migrating away from Functional Reactive Programming like RxSwift or Combine](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## When to Use AsyncAlgorithms
When migrating from Combine or RxSwift, you have multiple options for handling asynchronous patterns:
### Use AsyncAlgorithms for:
- **Time-based operations**: debounce, throttle, timers
- **Combining multiple async sequences**: merge, combineLatest, zip
- **Multi-consumer scenarios**: AsyncChannel for backpressure
- **Complex operator chains**: FRP-like patterns in Swift Concurrency
- **Specific operators**: removeDuplicates, chunks, adjacentPairs, compacted
### Use Standard Library for:
- **Bridging callbacks**: AsyncStream is sufficient
- **Simple iteration**: for await in sequence
- **Single-value operations**: async/await
- **Basic transformations**: map, filter, contains
### Use SwiftUI for:
- **UI observation**: @Observable macro
- **State management**: @State, @Published properties
- **User interactions**: onChange, onReceive modifiers
> **See**: [async-algorithms.md](async-algorithms.md) for detailed AsyncAlgorithms usage examples.
---
## Real-World Migration Examples
### Example: ArticleSearcher with AsyncAlgorithms
**Before: Manual Debouncing**
```swift
final class ArticleSearcher {
@MainActor private(set) var results: [Article] = []
private var currentSearchTask: Task<Void, Never>?
func search(_ query: String) {
currentSearchTask?.cancel()
currentSearchTask = Task {
do {
try await Task.sleep(for: .milliseconds(500))
await MainActor.run {
self.results = []
}
self.results = await APIClient.searchArticles(query)
} catch {
// Search was cancelled
}
}
}
}
// SwiftUI integration
struct SearchView: View {
@State private var searchQuery = ""
@State private var searcher = ArticleSearcher()
var body: some View {
List(searcher.results) { result in
Text(result.title)
}
.searchable(text: $searchQuery)
.onChange(of: searchQuery) { _, newValue in
searcher.search(newValue)
}
}
}
```
**After: AsyncAlgorithms Debounce**
```swift
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)
}
}
}
}
// SwiftUI integration
struct SearchView: View {
@State private var searchQuery = ""
@State private var searcher = ArticleSearcher()
var body: some View {
List(searcher.results) { result in
Text(result.title)
}
.searchable(text: $searchQuery)
.onChange(of: searchQuery) { _, newValue in
searcher.search(newValue)
}
.onAppear {
searcher.startDebouncedSearch()
}
}
}
```
**Benefits of using AsyncAlgorithms**:
- Automatic cancellation when new values arrive
- Backpressure handling (producer respects consumer pace)
- Cleaner code than manual Task.sleep management
- No need to track and cancel tasks manually
### Example: Notification Stream Migration
**Before: Combine Publisher**
```swift
import Combine
final class NotificationObserver: ObservableObject {
@Published private(set) var notifications: [AppNotification] = []
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
.compactMap { notification in
notification.object as? AppNotification
}
.receive(on: DispatchQueue.main)
.assign(to: &$notifications)
}
}
```
**After: Standard Library Notifications**
```swift
@Observable
final class NotificationObserver {
@MainActor private(set) var notifications: [AppNotification] = []
func startObserving() {
Task {
for await notification in NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification) {
if let appNotification = notification.object as? AppNotification {
notifications.append(appNotification)
}
}
}
}
}
```
**When to use each approach**:
- Use `notifications(named:)` for standard system notifications
- Use `AsyncChannel` for custom multi-consumer notification scenarios
- Use `@Observable` + SwiftUI for UI state updates
### Example: Multi-Source Data Loading
**Before: Combine Merge**
```swift
import Combine
final class MultiSourceLoader: ObservableObject {
@Published private(set) var items: [Item] = []
private var cancellables = Set<AnyCancellable>()
func loadFromAllSources() {
let source1 = APIClient.fetchItems(from: .source1)
let source2 = APIClient.fetchItems(from: .source2)
let source3 = APIClient.fetchItems(from: .source3)
Publishers.Merge3(source1, source2, source3)
.flatMap { items in
Just(items)
.delay(for: .seconds(0.1), scheduler: DispatchQueue.main)
}
.scan([]) { accumulated, new in
accumulated + new
}
.receive(on: DispatchQueue.main)
.assign(to: &$items)
.store(in: &cancellables)
}
}
```
**After: AsyncAlgorithms Merge + TaskGroup**
```swift
import AsyncAlgorithms
@Observable
final class MultiSourceLoader {
@MainActor private(set) var items: [Item] = []
func loadFromAllSources() async {
let sources = [
APIClient.fetchItems(from: .source1),
APIClient.fetchItems(from: .source2),
APIClient.fetchItems(from: .source3)
]
Task { @MainActor in
for await stream in sources.map { $0.values }.merge() {
for await newItems in stream {
self.items.append(contentsOf: newItems)
}
}
}
}
// Alternative: Using TaskGroup for parallel execution
func loadFromAllSourcesParallel() async {
await withTaskGroup(of: [Item].self) { group in
group.addTask {
await APIClient.fetchItems(from: .source1)
}
group.addTask {
await APIClient.fetchItems(from: .source2)
}
group.addTask {
await APIClient.fetchItems(from: .source3)
}
for await newItems in group {
await MainActor.run {
self.items.append(contentsOf: newItems)
}
}
}
}
}
```
**Key differences**:
- Combine `merge()` combines publishers; AsyncAlgorithms `merge()` combines sequences
- For parallel execution, use `TaskGroup` instead of `flatMap`
- State updates can use `@MainActor` instead of `.receive(on:)`
---
## Anti-Patterns to Avoid
### ❌ Don't Use Task.sleep for Debouncing
```swift
// ❌ Bad: Manual debouncing without backpressure
func search(_ query: String) {
Task {
try? await Task.sleep(for: .milliseconds(500))
await performSearch(query)
}
}
```
**Problem**: Every keystroke spawns a new task. If user types fast, multiple tasks execute simultaneously after 500ms, causing out-of-order results and wasted API calls.
**Solution**: Use `debounce()` from AsyncAlgorithms for automatic backpressure and cancellation.
### ❌ Don't Manually Combine Values
```swift
// ❌ Bad: Manual combination without operator
actor FormValidator {
private var currentUsername: String = ""
private var currentEmail: String = ""
private var currentPassword: String = ""
func updateUsername(_ username: String) {
currentUsername = username
checkForm()
}
func updateEmail(_ email: String) {
currentEmail = email
checkForm()
}
func updatePassword(_ password: String) {
currentPassword = password
checkForm()
}
private func checkForm() {
let state = validate(
username: currentUsername,
email: currentEmail,
password: currentPassword
)
// Update UI or emit validation state
}
}
```
**Problems**:
- More state management
- Boilerplate code for each field
- Harder to add new fields
- No stream composition benefits
**Solution**: Use `combineLatest()` for cleaner, composable validation.
### ❌ Don't Share Streams Without AsyncChannel
```swift
// ❌ Bad: Multiple consumers sharing same stream
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**: Values are split between consumers unpredictably. Each value goes to only one consumer.
**Solution**: Use `AsyncChannel` for true multi-consumer scenarios with backpressure.
---
---
## Concurrency-Safe Notifications (iOS 26+)
Swift 6.2 introduces **typed, thread-safe notifications**.
### MainActorMessage
For notifications that should be delivered on the main actor:
```swift
// Old way
NotificationCenter.default.addObserver(
forName: UIApplication.didBecomeActiveNotification,
object: nil,
queue: .main
) { [weak self] _ in
self?.handleDidBecomeActive() // ⚠️ Concurrency warning
}
// New way (iOS 26+)
token = NotificationCenter.default.addObserver(
of: UIApplication.self,
for: .didBecomeActive
) { [weak self] message in
self?.handleDidBecomeActive() // ✅ No warning, guaranteed main actor
}
```
**Key difference**: Observer closure is guaranteed to run on `@MainActor`.
### AsyncMessage
For notifications delivered asynchronously on arbitrary isolation:
```swift
struct RecentBuildsChangedMessage: NotificationCenter.AsyncMessage {
typealias Subject = [RecentBuild]
let recentBuilds: Subject
}
// Enable static member lookup
extension NotificationCenter.MessageIdentifier
where Self == NotificationCenter.BaseMessageIdentifier<RecentBuildsChangedMessage> {
static var recentBuildsChanged: NotificationCenter.BaseMessageIdentifier<RecentBuildsChangedMessage> {
.init()
}
}
```
**Posting**:
```swift
let builds = [RecentBuild(appName: "Stock Analyzer")]
let message = RecentBuildsChangedMessage(recentBuilds: builds)
NotificationCenter.default.post(message)
```
**Observing**:
```swift
// Old way: Unsafe casting
NotificationCenter.default.addObserver(forName: .recentBuildsChanged, object: nil, queue: nil) { notification in
guard let builds = notification.object as? [RecentBuild] else { return }
handleBuilds(builds)
}
// New way: Strongly typed, thread-safe
token = NotificationCenter.default.addObserver(
of: [RecentBuild].self,
for: .recentBuildsChanged
) { message in
handleBuilds(message.recentBuilds) // ✅ Direct access, no casting
}
```
**Benefits**:
- Strongly typed (no `Any` casting)
- Compile-time thread safety
- Clear isolation guarantees
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.10: Migrating to concurrency-safe notifications](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Common Challenges
### "It's Too Much Work"
Break it down:
- Migrate one package at a time
- Use 30-minute daily sessions
- Create checkpoints with small PRs
- Celebrate incremental progress
### "My Team Isn't Ready"
Start small:
- Enable Swift 6 for new files only
- Make new types `Sendable` by default
- Share learnings in team meetings
- Pair program on tricky migrations
### "Dependencies Aren't Ready"
Options:
- Update to latest versions first
- Use `@preconcurrency` temporarily
- Contribute fixes to open-source dependencies
- Wrap third-party APIs in your own concurrency-safe layer
### "I Keep Going in Circles"
This is the "concurrency rabbit hole":
- Take breaks and revisit later
- Temporarily disable strict checking to make progress elsewhere
- Focus on one module at a time
- Don't try to fix everything at once
> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.11: Frequently Asked Questions (FAQ) around Swift 6 Migrations](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
---
## Common Mistakes Agents Make
- **Blanket `@MainActor`**: Do not slap `@MainActor` on everything to silence errors. Ask whether the code truly needs main-actor isolation.
- **Mixing migration with unrelated refactors**: Focus solely on concurrency changes. Architectural improvements belong in separate PRs.
- **Using `@unchecked Sendable` as a first response**: Prefer immutable value types or actors. Reserve escape hatches for documented, temporary exceptions.
- **Giving pre-Swift 6.2 execution advice without checking the active feature set**: `nonisolated async` behavior depends on whether `NonisolatedNonsendingByDefault` is enabled.
- **Using Approachable Concurrency without migrating feature-by-feature first**: Enable individual upcoming features before the full bundle to understand each change's impact.
## Summary
Migration to Swift 6 is a journey, not a sprint:
1. **Start small**: Find isolated code with minimal dependencies
2. **Be incremental**: Use the three strict concurrency levels (Minimal → Targeted → Complete)
3. **Use tooling**: Leverage Xcode refactoring and `swift package migrate`
4. **Create checkpoints**: Small, focused PRs that can be merged quickly
5. **Stay positive**: The concurrency rabbit hole is real, but manageable with the right habits
6. **Think differently**: Let go of the threading mindset; trust the compiler
The result is **compile-time thread safety**, more maintainable code, and a future-proof codebase.
**Additional resources**:
- [Approachable Concurrency Video](https://youtu.be/y_Qc8cT-O_g?si=y4C1XQDGtyIOLW81)
- [Migration Tooling Video](https://youtu.be/FK9XFxSWZPg?si=2z_ybn1t1YCJow5k)
- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com) for in-depth migration strategies
references/performance.md
# Performance
Use this when:
- Async code is slower than expected or causing UI hangs.
- You need to choose between synchronous, asynchronous, and parallel execution.
- You are profiling concurrency overhead with Instruments.
Skip this file if:
- The issue is a compiler diagnostic about isolation or Sendable. Use `actors.md` or `sendable.md`.
- You mainly need to fix a memory leak. Use `memory-management.md`.
Jump to:
- Core Principles
- Common Performance Issues
- Using Xcode Instruments
- Suspension Points / Reducing Suspensions
- Choosing Execution Style
- Parallelism Costs
- Optimization Checklist
## Core Principles
### Measurement is essential
Can't improve what you don't measure. Establish baseline before optimizing.
### Start simple, optimize later
```
Synchronous → Asynchronous → Parallel
```
Move right only when proven necessary.
### Three phases of concurrency
1. **No concurrency** - Synchronous method
2. **Suspend without parallelism** - Asynchronous method
3. **Advanced concurrency** - Parallel execution
## Common Performance Issues
### UI hangs
Too much work on main thread causes interface freezes.
### Poor parallelization
Heavy work funneled into single task instead of parallel execution.
### Actor contention
Tasks waiting on busy actor, causing unnecessary suspensions.
## Using Xcode Instruments
### Swift Concurrency template
Profile with CMD + I → Select "Swift Concurrency" template.
**Instruments included**:
- **Swift Tasks**: Track running, alive, total tasks
- **Swift Actors**: Show actor execution and queue size
### Key metrics
```
Tasks:
- Total count
- Running vs suspended
- Task states (Creating, Running, Suspended, Ending)
Actors:
- Queue size
- Execution time
- Contention points
Main Thread:
- Hangs
- Blocked time
```
### Task states
- **Creating**: Task being initialized
- **Running**: Actively executing
- **Suspended**: Waiting (at await)
- **Ending**: Completing
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.1: Using Xcode Instruments to find performance bottlenecks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Identifying Issues
### Main thread blocked
```swift
// ❌ All work on main thread
@MainActor
func generateWallpapers() {
Task {
for _ in 0..<100 {
let image = generator.generate() // Blocks main thread
wallpapers.append(image)
}
}
}
```
**Instruments shows**: Long main thread hang, no parallelism.
### Solution: Move to background
```swift
@MainActor
func generateWallpapers() {
Task {
for _ in 0..<100 {
let image = await backgroundGenerator.generate()
wallpapers.append(image)
}
}
}
actor BackgroundGenerator {
func generate() -> Image {
// Heavy work in background
}
}
```
### Actor contention
```swift
actor Generator {
func generate() -> Image {
// Heavy work
}
}
// ❌ Sequential through actor
for _ in 0..<100 {
let image = await generator.generate() // Queue size = 1
}
```
**Instruments shows**: Actor queue never exceeds 1, no parallelism.
### Solution: Remove unnecessary actor
```swift
struct Generator {
@concurrent
static func generate() async -> Image {
// Heavy work, no shared state
}
}
// ✅ Parallel execution
for i in 0..<100 {
Task(name: "Image \(i)") {
let image = await Generator.generate()
await addToCollection(image)
}
}
```
## Suspension Points
### What creates suspension
Every `await` is potential suspension point:
```swift
let data = await fetchData() // May suspend
```
**Not guaranteed** - if isolation matches, may not suspend.
### Suspension surface area
Code between suspension points. Larger = harder to reason about:
- Actor invariants
- Performance
- Thread hops
- Reentrancy
- State consistency
### Goal
- Do work before crossing isolation
- Cross once
- Finish job
- Only cross again when necessary
## Reducing Suspensions
### 1. Use synchronous methods
```swift
// ❌ Unnecessary async
private func scale(_ image: CGImage) async { }
func process(_ image: CGImage) async {
let scaled = await scale(image) // Suspension point
}
// ✅ Synchronous helper
private func scale(_ image: CGImage) { }
func process(_ image: CGImage) async {
let scaled = scale(image) // No suspension
}
```
**Rule**: If method doesn't need to suspend, don't mark async.
### 2. Prevent actor reentrancy
```swift
// ❌ Reenters actor
actor BankAccount {
func deposit(_ amount: Int) async {
balance += amount
await logTransaction() // Leaves actor
balance += bonus // Reenters - state may have changed
}
}
// ✅ Complete work before leaving
actor BankAccount {
func deposit(_ amount: Int) async {
balance += amount
balance += bonus
await logTransaction() // Leave after state changes
}
}
```
### 3. Inherit isolation
```swift
// ❌ Switches isolation
@MainActor
func update() async {
await process() // Switches away from main actor
}
// ✅ Inherits isolation (still requires await -- but no executor hop)
@MainActor
func update() async {
await process() // Stays on main actor when nonisolated(nonsending)
}
nonisolated(nonsending) func process() async { }
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.2: Reducing suspension points by managing isolation effectively](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### 4. Use non-suspending APIs
```swift
// ❌ May suspend
try await Task.checkCancellation()
// ✅ No suspension
if Task.isCancelled {
return
}
```
### 5. Match Task entry isolation to its synchronous prefix
For unstructured `Task { ... }`, decide startup isolation from the synchronous prefix (everything before the first `await`). If that prefix needs main-actor access, keep inherited `@MainActor` entry. If the prefix does not need main actor, use `Task { @concurrent in ... }` and hop back with `MainActor.run` only when UI-owned mutation is required. A trivial non-main line (such as `print`) does **not** justify `@concurrent` when main-actor work already exists in the same prefix.
```swift
// ❌ 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()
}
```
Delayed retry is one specialization of this rule:
```swift
// ❌ Can wait for MainActor, then suspend immediately
registrationRetryTask = Task { @MainActor [weak self] in
try? await Task.sleep(for: .milliseconds(100))
guard let self else { return }
self.registrationRetryTask = nil
self.updateConnectedTargetWindow()
}
```
The delay itself is not UI work. Starting on `@MainActor` can add an avoidable executor wait before reaching `Task.sleep`, especially when scheduled from another executor or while the main actor is busy.
```swift
// ✅ Sleep off-main, hop back only for the UI-owned work
registrationRetryTask = Task { @concurrent [weak self] in
do {
try await Task.sleep(for: .milliseconds(100))
} catch is CancellationError {
return
}
guard let self else { return }
await MainActor.run {
self.registrationRetryTask = nil
self.updateConnectedTargetWindow()
}
}
```
Use this rule for any unstructured task: delayed retries, backoff, timer-like work, off-main computation, and actor hops. The key check is always “what runs before the first `await`?”, not “what does the task eventually do?”.
### 6. Embrace parallelism
```swift
// ❌ Sequential
for url in urls {
let image = await download(url)
images.append(image)
}
// ✅ Parallel
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
for await image in group {
images.append(image)
}
}
```
## Analyzing Suspensions in Instruments
### View task states
1. Select Swift Tasks instrument
2. Switch to "Task States" view
3. Look for Suspended states
4. Check suspension duration
### Navigate to code
1. Click task state (Running/Suspended)
2. Open Extended Detail
3. Click related method
4. Use "Open in Source Viewer"
### Predict suspensions
```swift
Task {
// State 1: Running
// State 2: Suspended (switch to background)
let data = await backgroundWork()
// State 3: Running (in background)
// State 4: Suspended (switch to main actor)
// State 5: Running (on main actor)
await MainActor.run {
updateUI(data)
}
}
```
### Optimization example
```swift
// Before: Two suspensions
Task {
let data = await generate() // Suspension 1
self.items.append(data) // Suspension 2 (back to main)
}
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.3: Using Xcode Instruments to detect and remove suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
// After: One suspension
Task { @concurrent in
let data = generate() // No suspension (synchronous)
await MainActor.run {
self.items.append(data) // Suspension 1 (to main)
}
}
```
## Choosing Execution Style
### Decision checklist
**Use async/parallel if**:
- [ ] Blocks main actor visibly (>16ms)
- [ ] Scales with data (N items → N cost)
- [ ] Involves I/O (network, disk)
- [ ] Benefits from combining operations
- [ ] Called frequently
**2+ checks** → async/parallel justified.
### Start synchronous
```swift
// Start here
func processData(_ data: Data) -> Result {
// Fast, in-memory work
}
```
**Only move to async if**:
- Instruments show main thread hang
- User reports sluggishness
- Work scales with input size
### When to use async
```swift
func processData(_ data: Data) async -> Result {
// Use when:
// - Touches persistent storage
// - Parses large datasets
// - Network communication
// - Proven slow by profiling
}
```
### When to use parallel
```swift
await withTaskGroup(of: Result.self) { group in
for item in items {
group.addTask { await process(item) }
}
}
// Use when:
// - Multiple independent operations
// - Time-to-first-result matters
> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.4: How to choose between serialized, asynchronous, and parallel execution](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
// - Work scales with collection size
// - Proven beneficial by profiling
```
## Parallelism Costs
### Tradeoffs
**Benefits**:
- Faster completion (if CPU-bound)
- Better resource utilization
- Improved responsiveness
**Costs**:
- Increased memory pressure
- CPU scheduling overhead
- System resource saturation
- Battery drain
- Thermal impact
### When parallelism hurts
```swift
// ❌ Over-parallelization
for i in 0..<1000 {
Task { await lightWork(i) }
}
// Creates 1000 tasks for trivial work
```
**Better**: Batch work or use fewer tasks.
## UX-Driven Decisions
### Smooth animations > raw speed
```swift
// 80ms on main thread, but animation stutters
@MainActor
func process() {
heavyWork() // Freezes UI for 1 frame
}
// 100ms total, but smooth UI
@MainActor
func process() async {
await backgroundWork() // UI stays responsive
}
```
**Perception**: Smooth feels faster than raw speed.
### Progress indication
```swift
@MainActor
func loadItems() async {
isLoading = true
for i in 0..<100 {
let item = await fetchItem(i)
items.append(item)
progress = Double(i) / 100 // Incremental updates
}
isLoading = false
}
```
Background work + progress = feels faster.
## Optimization Checklist
Before optimizing, ask:
- [ ] Have I profiled with Instruments?
- [ ] Is main thread actually blocked?
- [ ] Can this be synchronous?
- [ ] Am I over-parallelizing?
- [ ] Is actor contention the issue?
- [ ] Are suspensions necessary?
- [ ] Does UX require background work?
- [ ] Will this scale with data?
Anti-patterns to avoid with unstructured tasks:
- Starting on inherited `@MainActor` when nothing in the synchronous prefix (before first `await`) needs main actor.
- Moving trivial non-main lines off `@MainActor` when the same synchronous prefix already includes required main-actor mutation.
## Common Patterns
### Move heavy work to background
```swift
// Before
@MainActor
func generate() {
for _ in 0..<100 {
let item = heavyGeneration()
items.append(item)
}
}
// After
@MainActor
func generate() async {
for _ in 0..<100 {
let item = await backgroundGenerate()
items.append(item)
}
}
@concurrent
func backgroundGenerate() async -> Item {
// Heavy work off main thread
}
```
### Parallelize independent work
```swift
// Before: Sequential
for url in urls {
let image = await download(url)
images.append(image)
}
// After: Parallel
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
for await image in group {
images.append(image)
}
}
```
### Reduce actor hops
```swift
// Before: Multiple hops
actor Store {
func process() async {
let a = await fetch1() // Hop 1
let b = await fetch2() // Hop 2
let c = await fetch3() // Hop 3
combine(a, b, c)
}
}
// After: Batch fetches
actor Store {
func process() async {
async let a = fetch1()
async let b = fetch2()
async let c = fetch3()
combine(await a, await b, await c) // One hop
}
}
```
## Best Practices
1. **Profile before optimizing** - measure baseline
2. **Start synchronous** - add async only when needed
3. **Use Instruments regularly** - catch issues early
4. **Name tasks** - easier debugging in Instruments
5. **Check suspension count** - reduce unnecessary awaits
6. **Avoid premature parallelism** - has costs
7. **Consider UX** - smooth > fast
8. **Batch actor work** - reduce contention
9. **Test on real devices** - simulators lie
10. **Monitor in production** - real usage patterns differ
## Debugging Performance
### Instruments workflow
1. Profile with Swift Concurrency template
2. Identify main thread hangs
3. Check task parallelism
4. Analyze actor queue sizes
5. Review suspension points
6. Navigate to problematic code
7. Apply optimizations
8. Re-profile to verify
### Red flags in Instruments
- Main thread blocked >16ms
- Actor queue size always 1
- High suspension count
- Tasks created but not running
- Excessive task creation (1000+)
## Further Learning
For real-world optimization examples, profiling techniques, and advanced performance patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).
references/sendable.md
# Sendable
Use this when:
- A value or reference type must cross an isolation boundary safely.
- You are resolving "non-Sendable type" compiler diagnostics.
- You need to decide between value types, `@unchecked Sendable`, actors, or region-based isolation.
Skip this file if:
- The issue is about which actor should own the state. Use `actors.md`.
- The issue is about how async functions execute. Use `threading.md`.
Jump to:
- Isolation Domains
- Value Types (Structs, Enums)
- Reference Types (Classes)
- Functions and Closures (@Sendable)
- @unchecked Sendable
- Region-Based Isolation / `sending`
- Global Variables
- Decision Tree
## What is Sendable?
`Sendable` indicates a type is safe to share across isolation domains (actors, tasks, threads). The compiler verifies thread-safety at compile time.
```swift
public protocol Sendable {}
```
Empty protocol, but triggers compiler verification of thread-safety.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.1: Explaining the concept of Sendable in Swift](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Isolation Domains
Three types of isolation in Swift Concurrency:
### 1. Nonisolated (default)
No concurrency restrictions, but can't modify isolated state:
```swift
func computeValue(a: Int, b: Int) -> Int {
return a + b
}
```
### 2. Actor-isolated
Dedicated isolation domain with serialized access:
```swift
actor Library {
var books: [String] = []
func addBook(_ title: String) {
books.append(title)
}
}
// External access requires await
await library.addBook("Swift Concurrency")
```
### 3. Global actor-isolated
Shared isolation domain across types:
```swift
@MainActor
func updateUI() {
// Runs on main thread
}
```
## Data Races vs Race Conditions
### Data Race
Multiple threads access shared mutable state, at least one writes, without synchronization:
```swift
// ⚠️ Data race
var counter = 0
DispatchQueue.global().async { counter += 1 }
DispatchQueue.global().async { counter += 1 }
```
**Detection**: Enable Thread Sanitizer in scheme settings.
**Prevention**: Use actors or Sendable types:
```swift
actor Counter {
private var value = 0
func increment() {
value += 1
}
}
```
### Race Condition
Timing-dependent behavior leading to unpredictable results:
```swift
let counter = Counter()
for _ in 1...10 {
Task { await counter.increment() }
}
// May print inconsistent values
print(await counter.getValue())
```
**Key difference**: Swift Concurrency prevents data races but not race conditions. You must still ensure proper sequencing.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.2: Understanding Data Races vs. Race Conditions: Key Differences Explained](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Value Types (Structs, Enums)
### Implicit conformance
Non-public structs/enums with Sendable members:
```swift
// Implicitly Sendable
struct Person {
var name: String
}
```
### Explicit conformance required
Public types need explicit declaration:
```swift
public struct Person: Sendable {
var name: String
}
```
**Why**: Compiler can't verify internal details of public types across modules.
### Frozen types
Public frozen types can be implicitly Sendable:
```swift
@frozen
public struct Point: Sendable {
public var x: Double
public var y: Double
}
```
### All members must be Sendable
```swift
public struct Person: Sendable {
var name: String
var hometown: Location // Must also be Sendable
}
public struct Location: Sendable {
var name: String
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.3: Conforming your code to the Sendable protocol](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Copy-on-write makes mutability safe
```swift
public struct Person: Sendable {
var name: String // Mutable but safe due to COW
}
```
Each mutation creates a copy, preventing concurrent access to same instance.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.4: Sendable and Value Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Reference Types (Classes)
### Requirements for Sendable classes
Must be:
1. `final` (no inheritance)
2. Immutable stored properties only
3. All properties Sendable
4. No superclass or `NSObject` only
```swift
final class User: Sendable {
let name: String
let id: Int
init(name: String, id: Int) {
self.name = name
self.id = id
}
}
```
### Why non-final classes can't be Sendable
Child classes could introduce unsafe mutability:
```swift
// Can't be Sendable
class Purchaser {
func purchase() { }
}
// Could introduce data races
class GamePurchaser: Purchaser {
var credits: Int = 0 // Mutable!
}
```
### Actor isolation makes classes Sendable
```swift
@MainActor
class ViewModel {
var data: [Item] = [] // Safe due to actor isolation
}
// Implicitly Sendable
```
### Composition over inheritance
```swift
final class Purchaser: Sendable {
func purchase() { }
}
final class GamePurchaser {
let purchaser: Purchaser = Purchaser()
// Handle credits separately
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.5: Sendable and Reference Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Functions and Closures (@Sendable)
Mark functions/closures that cross isolation domains:
```swift
actor ContactsStore {
func removeAll(_ shouldRemove: @Sendable (Contact) -> Bool) async {
contacts.removeAll { shouldRemove($0) }
}
}
```
### Captured values must be Sendable
```swift
let query = "search"
// ✅ Immutable capture
store.filter { contact in
contact.name.contains(query)
}
var query = "search"
// ❌ Mutable capture
store.filter { contact in
contact.name.contains(query) // Error
}
```
### Capture lists for mutable values
```swift
var query = "search"
// ✅ Capture immutable snapshot
store.filter { [query] contact in
contact.name.contains(query)
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.6: Using @Sendable with closures](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## @unchecked Sendable
**Use as last resort.** Tells compiler to skip verification—you guarantee thread-safety.
### When to use
Manual locking mechanisms the compiler can't verify:
```swift
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var items: [String: Data] = [:]
func get(_ key: String) -> Data? {
lock.lock()
defer { lock.unlock() }
return items[key]
}
func set(_ key: String, value: Data) {
lock.lock()
defer { lock.unlock() }
items[key] = value
}
}
```
### Risks
- No compile-time safety
- Easy to introduce data races
- Must manually ensure all access uses lock
```swift
final class Cache: @unchecked Sendable {
private let lock = NSLock()
private var items: [String: Data] = [:]
// ⚠️ Forgot lock - data race!
var count: Int {
items.count
}
}
```
**Better**: Use actor instead:
```swift
actor Cache {
private var items: [String: Data] = [:]
var count: Int { items.count }
func get(_ key: String) -> Data? {
items[key]
}
func set(_ key: String, value: Data) {
items[key] = value
}
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.7: Using @unchecked Sendable](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Region-Based Isolation
Compiler allows non-Sendable types in same scope:
```swift
class Article {
var title: String
init(title: String) { self.title = title }
}
func check() {
let article = Article(title: "Swift")
Task {
print(article.title) // ✅ OK - same region
}
}
```
**Why**: No mutation after transfer, so no data race risk.
### Breaks when accessed after transfer
```swift
func check() {
let article = Article(title: "Swift")
Task {
print(article.title)
}
print(article.title) // ❌ Error - accessed after transfer
}
```
## The sending Keyword
Enforces ownership transfer for non-Sendable types:
### Parameter values
```swift
actor Logger {
func log(article: Article) {
print(article.title)
}
}
func printTitle(article: sending Article) async {
let logger = Logger()
await logger.log(article: article)
}
// Usage
let article = Article(title: "Swift")
await printTitle(article: article)
// article no longer accessible here
```
### Return values
```swift
@SomeActor
func createArticle(title: String) -> sending Article {
return Article(title: title)
}
```
Transfers ownership to caller's region.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.8: Understanding region-based isolation and the sending keyword](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Global Variables
Must be concurrency-safe since accessible from any context.
### Problem
```swift
class ImageCache {
static var shared = ImageCache() // ⚠️ Not concurrency-safe
}
```
### Solution 1: Actor isolation
```swift
@MainActor
class ImageCache {
static var shared = ImageCache()
}
```
### Solution 2: Immutable + Sendable
```swift
final class ImageCache: Sendable {
static let shared = ImageCache()
}
```
### Solution 3: nonisolated(unsafe)
**Last resort** - you guarantee safety:
```swift
struct APIProvider: Sendable {
nonisolated(unsafe) static private(set) var shared: APIProvider!
static func configure(apiURL: URL) {
shared = APIProvider(apiURL: apiURL)
}
}
```
Use `private(set)` to limit mutation points.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.9: Concurrency-safe global variables](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Custom Locks + Sendable
### Legacy code with locks
```swift
final class BankAccount: @unchecked Sendable {
private var balance: Int = 0
private let lock = NSLock()
func deposit(amount: Int) {
lock.lock()
balance += amount
lock.unlock()
}
func getBalance() -> Int {
lock.lock()
defer { lock.unlock() }
return balance
}
}
```
### Migration strategy
**New code**: Use actors
**Existing code**:
1. If isolated and small scope → migrate to actor
2. If widely used → use `@unchecked Sendable`, file migration ticket
```swift
// Better: Migrate to actor
actor BankAccount {
private var balance: Int = 0
func deposit(amount: Int) {
balance += amount
}
func getBalance() -> Int {
balance
}
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.10: Combining Sendable with custom Locks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Decision Tree
```
Need to share type across isolation domains?
├─ Value type (struct/enum)?
│ ├─ Public? → Add explicit Sendable
│ └─ Internal? → Implicit Sendable (if members Sendable)
│
├─ Reference type (class)?
│ ├─ Can be final + immutable? → Sendable
│ ├─ Needs mutation?
│ │ ├─ Can use actor? → Use actor (automatic Sendable)
│ │ ├─ Main thread only? → @MainActor
│ │ └─ Has custom lock? → @unchecked Sendable (temporary)
│ └─ Can be struct instead? → Refactor to struct
│
└─ Function/closure? → @Sendable attribute
```
## Common Patterns
### Restructure to avoid non-Sendable dependencies
```swift
// Instead of storing non-Sendable type
public struct Person: Sendable {
var hometown: String // Just the name
init(hometown: Location) {
self.hometown = hometown.name
}
}
```
### Prefer actors for mutable state
```swift
// Instead of @unchecked Sendable with locks
actor Cache {
private var items: [String: Data] = [:]
func get(_ key: String) -> Data? {
items[key]
}
}
```
### Use @MainActor for UI-bound types
```swift
@MainActor
class ViewModel: ObservableObject {
@Published var items: [Item] = []
}
```
## Best Practices
1. **Prefer value types** - structs/enums are easier to make Sendable
2. **Use actors for mutable state** - automatic thread-safety
3. **Avoid @unchecked Sendable** - use only for proven thread-safe code
4. **Mark public types explicitly** - don't rely on implicit conformance
5. **Ensure all members Sendable** - one non-Sendable breaks the chain
6. **Use @MainActor for UI types** - simple isolation for view models
7. **Capture immutably** - use capture lists for mutable variables
8. **Test with Thread Sanitizer** - catches runtime data races
9. **File migration tickets** - track @unchecked Sendable usage
## Further Learning
For migration strategies, real-world examples, and actor patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).
references/tasks.md
# Tasks
Use this when:
- You need to start async work from synchronous code.
- You are choosing between `Task`, `async let`, and task groups.
- You need cancellation, priorities, or structured vs unstructured guidance.
Skip this file if:
- The problem is mainly actor isolation or sendability. Use `actors.md` or `sendable.md`.
- The work is stream-shaped. Use `async-sequences.md` or `async-algorithms.md`.
Jump to:
- What is a Task?
- Cancellation
- Task Groups
- Discarding Task Groups
- Advanced: Task Timeout Pattern
- SwiftUI Integration
- Structured vs Unstructured Tasks
- Task Priorities
## What is a Task?
Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation—no `resume()` needed.
```swift
func synchronousMethod() {
Task {
await someAsyncMethod()
}
}
```
### Task entry isolation
`Task { ... }` inherits the enclosing isolation domain. This is especially easy to miss in modules that use `defaultIsolation(MainActor.self)` because bare tasks then start on `@MainActor` by default.
Choose task entry isolation using the synchronous prefix rule (everything before the first `await`):
- If the prefix needs main-actor work, keep inherited `@MainActor` entry.
- If the prefix does not need main actor, prefer `Task { @concurrent in ... }` and hop back only for UI mutation.
```swift
// ❌ Prefix has no main-actor work; first await hops away
Task {
await someActor.refresh()
}
// ✅ Prefix needs @MainActor; keep inherited main start
Task {
print("debug") // trivial non-main line rides along
self.isLoading = true // main-actor state before first await
await fetchData()
}
```
For deeper guidance and expanded examples, see `threading.md#choosing-task-entry-isolation`.
## Task References
Storing a reference is optional but enables cancellation and result waiting:
```swift
final class ImageLoader {
var loadTask: Task<UIImage, Error>?
func load() {
loadTask = Task {
try await fetchImage()
}
}
deinit {
loadTask?.cancel()
}
}
```
Tasks run regardless of whether you keep a reference.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.1: Introduction to tasks in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Cancellation
### Checking for cancellation
Tasks must manually check for cancellation:
```swift
// Throws CancellationError if canceled
try Task.checkCancellation()
// Boolean check for custom handling
guard !Task.isCancelled else {
return fallbackValue
}
```
### Where to check
Add checks at natural breakpoints:
```swift
let task = Task {
// Before expensive work
try Task.checkCancellation()
let data = try await URLSession.shared.data(from: url)
// After network, before processing
try Task.checkCancellation()
return processData(data)
}
```
### Child task cancellation
Canceling a parent automatically notifies all children:
```swift
let parent = Task {
async let child1 = work(1)
async let child2 = work(2)
let results = try await [child1, child2]
}
parent.cancel() // Both children notified
```
Children must still check `Task.isCancelled` to stop work.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.2: Task cancellation](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Error Handling
Task error types are inferred from the operation:
```swift
// Can throw
let throwingTask: Task<String, Error> = Task {
throw URLError(.badURL)
}
// Cannot throw
let nonThrowingTask: Task<String, Never> = Task {
"Success"
}
```
### Awaiting results
```swift
do {
let result = try await task.value
} catch {
// Handle error
}
```
### Handling errors internally
```swift
let safeTask: Task<String, Never> = Task {
do {
return try await riskyOperation()
} catch {
return "Fallback value"
}
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.3: Error handling in Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## SwiftUI Integration
### The .task modifier
Automatically manages task lifetime with view lifecycle:
```swift
struct ContentView: View {
@State private var data: Data?
var body: some View {
Text(data?.description ?? "Loading...")
.task {
data = try? await fetchData()
}
}
}
```
Task cancels automatically when view disappears.
### Reacting to value changes
```swift
.task(id: searchQuery) {
await performSearch(searchQuery)
}
```
When `searchQuery` changes:
1. Previous task cancels
2. New task starts with updated value
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.12: Running tasks in SwiftUI](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Priority configuration
```swift
// High priority (default for SwiftUI)
.task(priority: .userInitiated) {
await fetchUserData()
}
// Lower priority for background work
.task(priority: .low) {
await trackAnalytics()
}
```
## Task Groups
Dynamic parallel task execution with compile-time unknown task count.
### Basic usage
```swift
await withTaskGroup(of: UIImage.self) { group in
for url in photoURLs {
group.addTask {
await downloadPhoto(url: url)
}
}
}
```
### Collecting results
```swift
let images = await withTaskGroup(of: UIImage.self) { group in
for url in photoURLs {
group.addTask { await downloadPhoto(url: url) }
}
return await group.reduce(into: []) { $0.append($1) }
}
```
### Error handling
```swift
let images = try await withThrowingTaskGroup(of: UIImage.self) { group in
for url in photoURLs {
group.addTask { try await downloadPhoto(url: url) }
}
// Iterate to propagate errors
var results: [UIImage] = []
for try await image in group {
results.append(image)
}
return results
}
```
**Critical**: Errors in child tasks don't automatically fail the group. Use iteration (`for try await`, `next()`, `reduce()`) to propagate errors.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.5: Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Early termination on error
```swift
try await withThrowingTaskGroup(of: Data.self) { group in
for id in ids {
group.addTask { try await fetch(id) }
}
// First error cancels remaining tasks
while let data = try await group.next() {
process(data)
}
}
```
### Cancellation
```swift
await withTaskGroup(of: Result.self) { group in
for item in items {
group.addTask { await process(item) }
}
// Cancel all remaining tasks
group.cancelAll()
}
```
Or prevent adding to canceled group:
```swift
let didAdd = group.addTaskUnlessCancelled {
await work()
}
```
## Discarding Task Groups
For fire-and-forget operations where results don't matter:
```swift
await withDiscardingTaskGroup { group in
group.addTask { await logEvent("user_login") }
group.addTask { await preloadCache() }
group.addTask { await syncAnalytics() }
}
```
### Benefits
- More memory efficient (doesn't store results)
- No `next()` calls needed
- Automatically waits for completion
- Ideal for side effects
### Error handling
```swift
try await withThrowingDiscardingTaskGroup { group in
group.addTask { try await uploadLog() }
group.addTask { try await syncSettings() }
}
// First error cancels group and throws
```
### Real-world pattern: Multiple notifications
```swift
extension NotificationCenter {
func notifications(named names: [Notification.Name]) -> AsyncStream<()> {
AsyncStream { continuation in
let task = Task {
await withDiscardingTaskGroup { group in
for name in names {
group.addTask {
for await _ in self.notifications(named: name) {
continuation.yield(())
}
}
}
}
continuation.finish()
}
continuation.onTermination = { _ in task.cancel() }
}
}
}
// Usage
for await _ in NotificationCenter.default.notifications(
named: [.userDidLogin, UIApplication.didBecomeActiveNotification]
) {
refreshData()
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.6: Discarding Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Structured vs Unstructured Tasks
### Structured (preferred)
Bound to parent, inherit context, automatic cancellation:
```swift
// async let
async let data1 = fetch(1)
async let data2 = fetch(2)
let results = await [data1, data2]
// Task groups
await withTaskGroup(of: Data.self) { group in
group.addTask { await fetch(1) }
group.addTask { await fetch(2) }
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.7: The difference between structured and unstructured tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Unstructured (use sparingly)
Independent lifecycle, manual cancellation:
```swift
// Regular task (unstructured but inherits priority)
let task = Task {
await doWork()
}
// Detached task (completely independent)
Task.detached(priority: .background) {
await cleanup()
}
```
## Detached Tasks
**Use as last resort.** They don't inherit:
- Priority
- Task-local values
- Cancellation state
```swift
Task.detached(priority: .background) {
await DirectoryCleaner.cleanup()
}
```
### When to use
- Independent background work
- No connection to parent needed
- Acceptable to complete after parent cancels
- No `self` references needed
**Prefer**: Task groups or `async let` for most parallel work.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.4: Detached Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Task Priorities
### Available priorities
```swift
.high // Immediate user feedback
.userInitiated // User-triggered work (same as .high)
.medium // Default for detached tasks
.utility // Longer-running, non-urgent
.low // Similar to .background
.background // Lowest priority
```
### Setting priority
```swift
Task(priority: .background) {
await prefetchData()
}
```
### Priority inheritance
Structured tasks inherit parent priority:
```swift
Task(priority: .high) {
async let result = work() // Also .high
await result
}
```
Detached tasks don't inherit:
```swift
Task(priority: .high) {
Task.detached {
// Runs at .medium (default)
}
}
```
### Priority escalation
System automatically elevates priority to prevent priority inversion:
- Actor waiting on lower-priority task
- High-priority task awaiting `.value` of lower-priority task
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.8: Managing Task priorities](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Task.sleep() vs Task.yield()
### Task.sleep()
Suspends for fixed duration, non-blocking:
```swift
try await Task.sleep(for: .seconds(5))
```
**Use for:**
- Debouncing user input
- Polling intervals
- Rate limiting
- Artificial delays
**Respects cancellation** (throws `CancellationError`)
### Task.yield()
Temporarily suspends to allow other tasks to run:
```swift
await Task.yield()
```
**Use for:**
- Testing async code
- Allowing cooperative scheduling
**Note**: If current task is highest priority, may resume immediately.
### Practical: Debounced search
```swift
func search(_ query: String) async {
guard !query.isEmpty else {
searchResults = allResults
return
}
do {
try await Task.sleep(for: .milliseconds(500))
searchResults = allResults.filter { $0.contains(query) }
} catch {
// Canceled (user kept typing)
}
}
// In SwiftUI
.task(id: searchQuery) {
await searcher.search(searchQuery)
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.10: Task.yield() vs. Task.sleep()](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## async let vs TaskGroup
| Feature | async let | TaskGroup |
|---------|-----------|-----------|
| Task count | Fixed at compile-time | Dynamic at runtime |
| Syntax | Lightweight | More verbose |
| Cancellation | Automatic on scope exit | Manual via `cancelAll()` |
| Use when | 2-5 known parallel tasks | Loop-based parallel work |
```swift
// async let: Known task count
async let user = fetchUser()
async let settings = fetchSettings()
let profile = Profile(user: await user, settings: await settings)
// TaskGroup: Dynamic task count
await withTaskGroup(of: Image.self) { group in
for url in urls {
group.addTask { await download(url) }
}
}
```
## Advanced: Task Timeout Pattern
Create timeout wrapper using task groups:
```swift
func withTimeout<T>(
_ duration: Duration,
operation: @Sendable @escaping () async throws -> T
) async throws -> T {
try await withThrowingTaskGroup(of: T.self) { group in
group.addTask { try await operation() }
group.addTask {
try await Task.sleep(for: duration)
throw TimeoutError()
}
guard let result = try await group.next() else {
throw TimeoutError()
}
group.cancelAll()
return result
}
}
// Usage
let data = try await withTimeout(.seconds(5)) {
try await slowNetworkRequest()
}
```
**`cancelAll()` is critical** — without it, the losing task keeps running until scope exit.
`Task.sleep` throws `CancellationError` when the task is cancelled, making it a useful cancellation checkpoint in polling loops. `Task.yield()` only gives other tasks a chance to run and does not check cancellation — if the current task has the highest priority, it may resume immediately.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.14: Creating a Task timeout handler using a Task Group (advanced)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Common Patterns
### Sequential with early exit
```swift
let user = try await fetchUser()
guard user.isActive else { return }
let posts = try await fetchPosts(userId: user.id)
```
### Parallel independent work
```swift
async let user = fetchUser()
async let settings = fetchSettings()
async let notifications = fetchNotifications()
let data = try await (user, settings, notifications)
```
### Mixed: Sequential then parallel
```swift
let user = try await fetchUser()
async let posts = fetchPosts(userId: user.id)
async let followers = fetchFollowers(userId: user.id)
let profile = Profile(
user: user,
posts: try await posts,
followers: try await followers
)
```
## Common Mistakes Agents Make
- Replacing structured child work with many unrelated top-level tasks.
- Using `Task.detached` just to "make it background."
- Ignoring cancellation in long-running operations.
- Keeping a stored task forever without a clear owner or cleanup path.
- Picking entry isolation from the enclosing context rather than the task's synchronous prefix — `Task { await someActor.x() }` from a `@MainActor` context should be `Task { @concurrent in ... }`; a `Task` whose prefix mutates `@MainActor` state should stay on inherited `@MainActor` even if it also has a `print`.
- Priorities are hints, not guarantees. The system automatically elevates priority to prevent inversion (e.g., a high-priority task awaiting `.value` of a lower-priority task). Do not rely on priority for correctness.
## Best Practices
1. **Check cancellation regularly** in long-running tasks
2. **Use structured concurrency** (avoid detached tasks)
3. **Leverage SwiftUI's `.task` modifier** for view-bound work
4. **Choose the right tool**: `async let` for fixed, TaskGroup for dynamic
5. **Handle errors explicitly** in throwing task groups
6. **Set priority only when needed** (inherit by default)
7. **Don't mutate task groups** from outside their creation context
## Further Learning
For hands-on examples, advanced patterns, and migration strategies, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).
references/testing.md
# Testing Concurrent Code
Use this when:
- You are writing async tests.
- A test is flaky because of task scheduling or actor isolation.
- You need to replace XCTest waiting APIs or verify deallocation.
Skip this file if:
- You mainly need production ownership guidance. Use `actors.md`, `tasks.md`, or `memory-management.md`.
Jump to:
- Swift Testing (Recommended)
- Awaiting Async Callbacks
- Setup and Teardown
- Handling Flaky Tests
- Swift Concurrency Extras
- XCTest Patterns (Legacy)
- Memory Management Tests
- Testing Checklist
## Recommendation: Use Swift Testing
**Swift Testing is strongly recommended** for new projects and tests. It provides:
- Modern Swift syntax with macros
- Better concurrency support
- Cleaner test structure
- More flexible test organization
XCTest patterns are included for legacy codebases.
## Swift Testing Basics
### Simple async test
```swift
@Test
@MainActor
func emptyQuery() async {
let searcher = ArticleSearcher()
await searcher.search("")
#expect(searcher.results == ArticleSearcher.allArticles)
}
```
**Key differences from XCTest**:
- `@Test` macro instead of `XCTestCase`
- `#expect` instead of `XCTAssert`
- Structs preferred over classes
- No `test` prefix required
### Testing with actors
```swift
@Test
@MainActor
func searchReturnsResults() async {
let searcher = ArticleSearcher()
await searcher.search("swift")
#expect(!searcher.results.isEmpty)
}
```
Mark test with actor if system under test requires it.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.2: Testing concurrent code using Swift Testing](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Awaiting Async Callbacks
### Using continuations
When testing unstructured tasks:
```swift
@Test
@MainActor
func searchTaskCompletes() async {
let searcher = ArticleSearcher()
await withCheckedContinuation { continuation in
_ = withObservationTracking {
searcher.results
} onChange: {
continuation.resume()
}
searcher.startSearchTask("swift")
}
#expect(searcher.results.count > 0)
}
```
**Use when**: Testing code that spawns unstructured tasks.
### Using confirmations
For structured async code:
```swift
@Test
@MainActor
func searchTriggersObservation() async {
let searcher = ArticleSearcher()
await confirmation { confirm in
_ = withObservationTracking {
searcher.results
} onChange: {
confirm()
}
// Must await here for confirmation to work
await searcher.search("swift")
}
#expect(!searcher.results.isEmpty)
}
```
**Critical**: Must `await` async work for confirmation to validate.
## Setup and Teardown
### Using init/deinit
```swift
@MainActor
final class DatabaseTests {
let database: Database
init() async throws {
database = Database()
await database.prepare()
}
deinit {
// Synchronous cleanup only
}
@Test
func insertsData() async throws {
try await database.insert(item)
#expect(await database.count() == 1)
}
}
```
**Limitation**: `deinit` cannot call async methods.
### Test Scoping Traits
For async teardown:
```swift
@MainActor
struct DatabaseTrait: SuiteTrait, TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: () async throws -> Void
) async throws {
let database = Database()
try await Environment.$database.withValue(database) {
await database.prepare()
try await function()
await database.cleanup() // Async teardown
}
}
}
// Environment for task-local storage
@MainActor
struct Environment {
@TaskLocal static var database = Database()
}
// Apply to suite
@Suite(DatabaseTrait())
@MainActor
final class DatabaseTests {
@Test
func insertsData() async throws {
try await Environment.database.insert(item)
}
}
// Or apply to individual test
@Test(DatabaseTrait())
func specificTest() async throws {
// Test code
}
```
**Use when**: Need async cleanup after each test.
## Handling Flaky Tests
### Problem: Race conditions
```swift
@Test
@MainActor
func isLoadingState() async throws {
let fetcher = ImageFetcher()
let task = Task { try await fetcher.fetch(url) }
// ❌ Flaky - may pass or fail
#expect(fetcher.isLoading == true)
try await task.value
#expect(fetcher.isLoading == false)
}
```
**Issue**: Task may complete before we check `isLoading`.
### Solution: Swift Concurrency Extras
```swift
import ConcurrencyExtras
@Test
@MainActor
func isLoadingState() async throws {
try await withMainSerialExecutor {
let fetcher = ImageFetcher { url in
await Task.yield() // Allow test to check state
return Data()
}
let task = Task { try await fetcher.fetch(url) }
await Task.yield() // Switch to task
#expect(fetcher.isLoading == true) // ✅ Reliable
try await task.value
#expect(fetcher.isLoading == false)
}
}
```
**Add package**: `https://github.com/pointfreeco/swift-concurrency-extras.git`
> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.3: Using Swift Concurrency Extras by Point-Free](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Serial execution required
```swift
@Suite(.serialized)
@MainActor
final class ImageFetcherTests {
// Tests run serially when using withMainSerialExecutor
}
```
**Critical**: Main serial executor doesn't work with parallel test execution.
## XCTest Patterns (Legacy)
### Basic async test
```swift
final class ArticleSearcherTests: XCTestCase {
@MainActor
func testEmptyQuery() async {
let searcher = ArticleSearcher()
await searcher.search("")
XCTAssertEqual(searcher.results, ArticleSearcher.allArticles)
}
}
```
### Using expectations
```swift
@MainActor
func testSearchTask() async {
let searcher = ArticleSearcher()
let expectation = expectation(description: "Search complete")
_ = withObservationTracking {
searcher.results
} onChange: {
expectation.fulfill()
}
searcher.startSearchTask("swift")
// Use fulfillment, not wait
await fulfillment(of: [expectation], timeout: 10)
XCTAssertEqual(searcher.results.count, 1)
}
```
**Critical**: Use `await fulfillment(of:)`, not `wait(for:)` to avoid deadlocks.
### Setup and teardown
```swift
final class DatabaseTests: XCTestCase {
override func setUp() async throws {
// Async setup
}
override func tearDown() async throws {
// Async teardown
}
}
```
Mark as `async throws` to call async methods.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.1: Testing concurrent code using XCTest](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
### Main serial executor for all tests
```swift
final class MyTests: XCTestCase {
override func invokeTest() {
withMainSerialExecutor {
super.invokeTest()
}
}
}
```
## Common Patterns
### Testing @MainActor code
```swift
@Test
@MainActor
func viewModelUpdates() async {
let viewModel = ViewModel()
await viewModel.loadData()
#expect(viewModel.items.count > 0)
}
```
### Testing actors
```swift
@Test
func actorIsolation() async {
let store = DataStore()
await store.insert(item)
let count = await store.count()
#expect(count == 1)
}
```
### Testing cancellation
```swift
@Test
func cancellationStopsWork() async throws {
let processor = DataProcessor()
let task = Task {
try await processor.processLargeDataset()
}
task.cancel()
do {
try await task.value
Issue.record("Should have thrown cancellation error")
} catch is CancellationError {
// Expected
}
}
```
### Testing with delays
```swift
@Test
func debouncedSearch() async throws {
try await withMainSerialExecutor {
let searcher = DebouncedSearcher()
searcher.search("a")
await Task.yield()
searcher.search("ab")
await Task.yield()
searcher.search("abc")
// Wait for debounce
try await Task.sleep(for: .milliseconds(600))
#expect(searcher.searchCount == 1) // Only last search executed
}
}
```
### Testing task groups
```swift
@Test
func taskGroupProcessesAll() async throws {
let processor = BatchProcessor()
let results = await withTaskGroup(of: Int.self) { group in
for i in 1...5 {
group.addTask { await processor.process(i) }
}
var collected: [Int] = []
for await result in group {
collected.append(result)
}
return collected
}
#expect(results.count == 5)
}
```
## Testing Memory Management
### Verify deallocation
```swift
@Test
func viewModelDeallocates() async {
var viewModel: ViewModel? = ViewModel()
weak var weakViewModel = viewModel
viewModel?.startWork()
viewModel = nil
try? await Task.sleep(for: .milliseconds(100))
#expect(weakViewModel == nil)
}
```
### Detect retain cycles
```swift
@Test
func noRetainCycle() async {
var manager: Manager? = Manager()
weak var weakManager = manager
manager?.startLongRunningTask()
manager = nil
#expect(weakManager == nil)
}
```
## Best Practices
1. **Use Swift Testing for new code** - modern, better concurrency support
2. **Mark tests with correct isolation** - @MainActor when needed
3. **Use confirmations over continuations** - when structured concurrency allows
4. **Serialize tests with main serial executor** - avoid flaky tests
5. **Test cancellation explicitly** - ensure proper cleanup
6. **Verify deallocation** - catch retain cycles early
7. **Use Task.yield() strategically** - control execution in tests
8. **Avoid sleep in tests** - use continuations/confirmations instead
9. **Test actor isolation** - verify thread safety
10. **Keep tests deterministic** - avoid timing dependencies
## Migration from XCTest
### XCTest → Swift Testing
```swift
// XCTest
final class MyTests: XCTestCase {
func testExample() async {
XCTAssertEqual(value, expected)
}
}
// Swift Testing
@Suite
struct MyTests {
@Test
func example() async {
#expect(value == expected)
}
}
```
### Expectations → Confirmations
```swift
// XCTest
let expectation = expectation(description: "Done")
doWork { expectation.fulfill() }
await fulfillment(of: [expectation])
// Swift Testing
await confirmation { confirm in
await doWork { confirm() }
}
```
### Setup/Teardown → Traits
```swift
// XCTest
override func setUp() async throws {
await prepare()
}
// Swift Testing
struct SetupTrait: TestTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: () async throws -> Void
) async throws {
await prepare()
try await function()
}
}
```
## Troubleshooting
### Test hangs
**Cause**: Waiting for expectation that never fulfills.
**Solution**: Add timeout, verify observation tracking.
### Flaky test
**Cause**: Race condition in unstructured task.
**Solution**: Use main serial executor + Task.yield().
### Deadlock
**Cause**: Using `wait(for:)` in async context.
**Solution**: Use `await fulfillment(of:)` instead.
### Confirmation fails
**Cause**: Not awaiting async work in confirmation block.
**Solution**: Add `await` before async calls.
### Actor isolation error
**Cause**: Test not marked with required actor.
**Solution**: Add `@MainActor` or appropriate actor to test.
## Common Mistakes Agents Make
- **Flaky intermediate-state assertions**: Asserting `isLoading == true` immediately after creating a `Task` is a race condition — the task may not have started yet. Use `withMainSerialExecutor` + `Task.yield()` to control scheduling before asserting intermediate state.
- **Using `Task.sleep` as a synchronization primitive** in tests instead of deterministic scheduling.
- **Asserting intermediate state without controlling scheduling**: Always use `withMainSerialExecutor` when you need to observe state between task creation and completion. Note: `withMainSerialExecutor` does not work with parallel test execution — mark the suite `@Suite(.serialized)`.
- **Reaching into isolated internals** instead of testing public behavior.
- **Keeping both Swift Testing and XCTest versions** of the same example unless they teach different migration paths.
## Testing Checklist
- [ ] Tests marked with correct isolation
- [ ] Using Swift Testing (recommended)
- [ ] Async methods properly awaited
- [ ] Cancellation tested
- [ ] Memory leaks checked
- [ ] Race conditions handled
- [ ] Timeouts appropriate
- [ ] Flaky tests fixed with serial executor
- [ ] Actor isolation verified
- [ ] Cleanup in traits (not deinit)
## Further Learning
For advanced testing patterns, real-world examples, and migration strategies:
- [Swift Testing Documentation](https://developer.apple.com/documentation/testing)
- [Swift Concurrency Extras](https://github.com/pointfreeco/swift-concurrency-extras)
- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com)
references/threading.md
# Threading
Use this when:
- You need to understand the relationship between tasks and threads.
- You are debugging suspension points, actor reentrancy, or unexpected execution contexts.
- You need Swift 6.2 behavior guidance (`nonisolated async`, `@concurrent`, `nonisolated(nonsending)`).
Skip this file if:
- You mainly need to protect mutable state. Use `actors.md`.
- You need to make types safe to transfer. Use `sendable.md`.
Jump to:
- Core Concepts (Tasks vs Threads)
- Cooperative Thread Pool
- Suspension Points and Actor Reentrancy
- Swift 6.2 Changes (SE-461, SE-466)
- Default Isolation Domain
- Debugging Thread Execution
- Common Misconceptions
- Migration Strategy
## Core Concepts
### What is a Thread?
System-level resource that runs instructions. High overhead for creation and switching. Swift Concurrency abstracts thread management away.
### Tasks vs Threads
**Tasks** are units of async work, not tied to specific threads. Swift dynamically schedules tasks on available threads from a cooperative pool.
**Key insight**: No direct relationship between one task and one thread.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.1: How Threads relate to Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
**Important (Swift 6+)**: Avoid using `Thread.current` inside async contexts. In Swift 6 language mode, `Thread.current` is unavailable from asynchronous contexts and will fail to compile. Prefer reasoning in terms of isolation domains; use Instruments and the debugger to observe execution when needed.
## Cooperative Thread Pool
Swift creates only as many threads as CPU cores. Tasks share these threads efficiently.
### How it works
1. **Limited threads**: Number matches CPU cores
2. **Task scheduling**: Tasks scheduled onto available threads
3. **Suspension**: At `await`, task suspends, thread freed for other work
4. **Resumption**: Task resumes on any available thread (not necessarily the same one)
```swift
func example() async {
print("Started on: \(Thread.current)")
try await Task.sleep(for: .seconds(1))
print("Resumed on: \(Thread.current)") // Likely different thread
}
```
### Benefits over GCD
**Prevents thread explosion**:
- No excessive thread creation
- No high memory overhead from idle threads
- No excessive context switching
- No priority inversion
**Better performance**:
- Fewer threads = less context switching
- Continuations instead of blocking
- CPU cores stay busy efficiently
## Threading Mindset → Isolation Mindset
### Old way (GCD)
```swift
// Thinking about threads
DispatchQueue.main.async {
// Update UI on main thread
}
DispatchQueue.global(qos: .background).async {
// Heavy work on background thread
}
```
### New way (Swift Concurrency)
```swift
// Thinking about isolation domains
@MainActor
func updateUI() {
// Runs on main actor (usually main thread)
}
func heavyWork() async {
// Runs on any available thread in pool
}
```
### Think in isolation domains
**Don't ask**: "What thread should this run on?"
**Ask**: "What isolation domain should own this work?"
- `@MainActor` for UI updates
- Custom actors for specific state
- Nonisolated for general async work
### Provide hints, not commands
```swift
Task(priority: .userInitiated) {
await doWork()
}
```
You're describing the nature of work, not assigning threads. Swift optimizes execution.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.2: Getting rid of the "Threading Mindset"](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Suspension Points
### What is a suspension point?
Moment where task **may** pause to allow other work. Marked by `await`.
```swift
let data = await fetchData() // Potential suspension
```
**Critical**: `await` marks *possible* suspension, not guaranteed. If operation completes synchronously, no suspension occurs.
### Why suspension points matter
1. **Code may pause unexpectedly** - resumes later, possibly different thread
2. **State can change** - mutable state may be modified during suspension
3. **Actor reentrancy** - other tasks can access actor during suspension
The same entry-isolation rule applies to any unstructured task: choose startup isolation by what the synchronous prefix needs. If nothing before the first `await` needs the main actor—whether that first operation is `Task.sleep`, an actor hop, a `print`, or a Sendable computation—prefer `Task { @concurrent in ... }` and hop back with `MainActor.run` only for the UI mutation. If the synchronous prefix already needs main actor for one statement, keep nearby cheap lines on main with it instead of splitting them out.
### Actor reentrancy example
```swift
actor BankAccount {
private var balance: Int = 0
func deposit(amount: Int) async {
balance += amount
print("Balance: \(balance)")
await logTransaction(amount) // ⚠️ Suspension point
balance += 10 // Bonus
print("After bonus: \(balance)")
}
func logTransaction(_ amount: Int) async {
try? await Task.sleep(for: .seconds(1))
}
}
// Two concurrent deposits
async let _ = account.deposit(amount: 100)
async let _ = account.deposit(amount: 100)
// Unexpected: 100 → 200 → 210 → 220
// Expected: 100 → 110 → 210 → 220
```
**Why**: During `logTransaction`, second deposit runs, modifying balance before first completes.
### Avoiding reentrancy bugs
**Complete actor work before suspending**:
```swift
func deposit(amount: Int) async {
balance += amount
balance += 10 // Bonus applied first
print("Final balance: \(balance)")
await logTransaction(amount) // Suspend after state changes
}
```
**Rule**: Don't mutate actor state after suspension points.
> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.3: Understanding Task suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
## Choosing Task entry isolation
For unstructured `Task { ... }`, choose entry isolation based on the synchronous prefix (everything before the first `await`), not on where the task was created.
Two common reasons a bare `Task { ... }` starts on `@MainActor`:
- The task is spawned from a `@MainActor` context.
- The module enables default main-actor isolation (for example, `defaultIsolation(MainActor.self)`).
Rule:
- If the synchronous prefix contains any main-actor work, keep inherited main-actor entry.
- If the synchronous prefix contains no main-actor work, start with `Task { @concurrent in ... }` and hop back to `MainActor` only when needed.
```swift
// ❌ 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()
}
```
The delayed-retry `Task.sleep` pattern (see `performance.md` "Match Task entry isolation to its synchronous prefix") is a specialization of this same rule: the wait is usually not UI-owned, while the final mutation is.
Note that `Task { @concurrent in ... }` changes the closure's isolation, so any capture of non-Sendable state from the enclosing actor must move inside the `MainActor.run { ... }` hop, or be captured weakly (e.g., `[weak self]` plus a `guard let self`) before being used there. The examples above stay safe by keeping `self` use inside `MainActor.run`. If the body needs to touch non-Sendable state directly, see `sendable.md` before reaching for `@concurrent`.
## Thread Execution Patterns
### Default: Background threads
Tasks run on cooperative thread pool (background threads):
```swift
Task {
print(Thread.current) // Background thread
}
```
### Main thread execution
Use `@MainActor` for main thread:
```swift
@MainActor
func updateUI() {
Task {
print(Thread.current) // Main thread
}
}
```
### Inheritance example
```swift
@MainActor
func updateUI() {
print("Main thread: \(Thread.current)")
await backgroundTask() // Switches to background
print("Back on main: \(Thread.current)") // Returns to main
}
func backgroundTask() async {
print("Background: \(Thread.current)")
}
```
## Swift 6.2 Changes
### Nonisolated async functions (SE-461)
**Old behavior**: Nonisolated async functions always switch to background.
**New behavior**: Inherit caller's isolation by default.
```swift
class NotSendable {
func performAsync() async {
print(Thread.current)
}
}
@MainActor
func caller() async {
let obj = NotSendable()
await obj.performAsync()
// Old: Background thread
// New: Main thread (inherits @MainActor)
}
```
### Enabling new behavior
In Xcode 16+:
```swift
// Build setting or swift-settings
.enableUpcomingFeature("NonisolatedNonsendingByDefault")
```
### Opting out with @concurrent
Force function to switch away from caller's isolation:
```swift
@concurrent
func performAsync() async {
print(Thread.current) // Always background
}
```
### nonisolated(nonsending)
Prevent sending non-Sendable values across isolation:
```swift
nonisolated(nonsending) func storeTouch(...) async {
// Runs on caller's isolation, no value sending
}
```
> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.4: Dispatching to different threads using nonisolated(nonsending) and @concurrent (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
**Use when**: Method doesn't need to switch isolation, avoiding Sendable requirements.
## Default Isolation Domain (SE-466)
### Configuring default isolation
**Build setting** (Xcode 16+):
- Default Actor Isolation: `MainActor` or `None`
**Swift Package**:
```swift
.target(
name: "MyTarget",
swiftSettings: [
.defaultIsolation(MainActor.self)
]
)
```
### Why change default?
Most app code runs on main thread. Setting `@MainActor` as default:
- Reduces false warnings
- Avoids "concurrency rabbit hole"
- Makes migration easier
### Inference with @MainActor default
```swift
// With @MainActor as default:
func f() {} // Inferred: @MainActor
class C {
init() {} // Inferred: @MainActor
static var value = 10 // Inferred: @MainActor
}
@MyActor
struct S {
func f() {} // Inferred: @MyActor (explicit override)
}
> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.5: Controlling the default isolation domain (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference)
```
### Per-module setting
Must opt in for each module/package. Not global across dependencies.
### Backward compatibility
Opt-in only. Default remains `nonisolated` if not specified.
## Debugging Thread Execution
### Print current thread
**⚠️ Important**: `Thread.current` is unavailable in Swift 6 language mode from async contexts. The compiler error states: "Class property 'current' is unavailable from asynchronous contexts; Thread.current cannot be used from async contexts."
**Workaround** (Swift 6+ mode only):
```swift
extension Thread {
public static var currentThread: Thread {
Thread.current
}
}
print("Thread: \(Thread.currentThread)")
```
### Debug navigator
1. Set breakpoint in task
2. Debug → Pause
3. Check Debug Navigator for thread info
### Verify main thread
```swift
assert(Thread.isMainThread)
```
## Common Misconceptions
### ❌ Each Task runs on new thread
**Wrong**. Tasks share limited thread pool, reuse threads.
### ❌ await blocks the thread
**Wrong**. `await` suspends task without blocking thread. Other tasks can use the thread.
### ❌ Task execution order is guaranteed
**Wrong**. Tasks execute based on system scheduling. Use `await` to enforce order.
### ❌ Same task = same thread
**Wrong**. Task can resume on different thread after suspension.
## Why Sendable Matters
Since tasks move between threads unpredictably:
```swift
func example() async {
print("Thread 1: \(Thread.current)")
await someWork()
print("Thread 2: \(Thread.current)") // Different thread
}
```
Values crossing suspension points may cross threads. **Sendable** ensures safety.
## Best Practices
1. **Stop thinking about threads** - think isolation domains
2. **Trust the system** - Swift optimizes thread usage
3. **Use @MainActor for UI** - clear, explicit main thread execution
4. **Minimize suspension points in actors** - avoid reentrancy bugs
5. **Complete state changes before suspending** - prevent inconsistent state
6. **Use priorities as hints** - not guarantees
7. **Make types Sendable** - safe across thread boundaries
8. **Enable Swift 6.2 features** - easier migration, better defaults
9. **Set default isolation for apps** - reduce false warnings
10. **Don't force thread switching** - let Swift optimize
## Migration Strategy
### For new projects (Xcode 16+)
1. Set default isolation to `@MainActor`
2. Enable `NonisolatedNonsendingByDefault`
3. Use `@concurrent` for explicit background work
### For existing projects
1. Gradually enable Swift 6 language mode
2. Consider default isolation change
3. Use `@concurrent` to maintain old behavior where needed
4. Migrate module by module
## Decision Tree
```
Need to control execution?
├─ UI updates? → @MainActor
├─ Specific state isolation? → Custom actor
├─ Background work? → Regular async (trust Swift)
└─ Need to force background? → @concurrent (Swift 6.2+)
Seeing Sendable warnings?
├─ Can make type Sendable? → Add conformance
├─ Same isolation OK? → nonisolated(nonsending)
└─ Need different isolation? → Make Sendable or refactor
```
## GCD to Isolation Domain Migration
Instead of asking "what thread should this run on?" ask "what isolation domain should own this work?"
- `DispatchQueue.main.async { }` → `@MainActor func updateUI()`
- `DispatchQueue.global().async { }` → `func work() async` (or `@concurrent` if it must leave caller isolation)
- `DispatchQueue(label:).sync { }` → `actor` or `Mutex` for protecting state
- Serial queue for ordering → `actor` (guarantees serial access)
## Decision Rules
- UI state → usually `@MainActor`
- Mutable shared state → usually an `actor`
- Plain async work with no isolated state → `async` API with explicit ownership
- Work that must hop away from caller isolation under Swift 6.2-era behavior → consider `@concurrent`
## Common Mistakes Agents Make
- Recommending GCD queue hopping when actor isolation already expresses the ownership model.
- Debugging correctness by thread ID instead of by isolation and ordering.
- Treating `await` as a blocking call — it suspends the task, freeing the thread.
- Mapping each `Task` to a conceptual thread.
- Picking task entry isolation by the enclosing context instead of by the task's synchronous prefix. A `Task { ... }` from `@MainActor` whose first `await` immediately hops away (with no main-actor work before it) should usually be `Task { @concurrent in ... }`.
## Performance Insights
### Why fewer threads = better performance
- **Less context switching**: CPU spends more time on actual work
- **Better cache utilization**: Threads stay on same cores longer
- **No thread explosion**: Predictable resource usage
- **Forward progress**: Threads never block, always productive
### Cooperative pool advantages
- Matches hardware (one thread per core)
- Prevents oversubscription
- Efficient task scheduling
- Automatic load balancing
## Further Learning
For migration strategies, real-world examples, and advanced threading patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com).