---
name: swift-concurrency
description: >
  Write, review, or fix Swift 6 concurrency code using actors, Sendable, structured concurrency,
  and the strict data-race-safety model. Use when working with async/await, actors, data race
  compiler errors, Sendable conformance, TaskGroup, AsyncSequence, or migrating Swift 5 code
  to Swift 6 strict concurrency. Triggers on: "actor", "Sendable", "data race", "MainActor",
  "async let", "TaskGroup", "AsyncStream", "concurrency warning", "strict concurrency",
  "Swift 6 concurrency", "nonisolated", "Mutex".
---

# Swift 6 Concurrency

## Overview

Write, review, and fix Swift 6 concurrency code in this iOS project. Swift 6 enforces complete data race safety at compile time — every piece of mutable state that crosses a concurrency domain boundary must be protected by actors, `Sendable` conformance, or `Mutex`. This skill applies to all modules: App, Data, Domain, and Model.

## Agent Behavior Contract

1. **Never suppress concurrency errors with `nonisolated(unsafe)` without explicit user approval.** Investigate and fix the isolation root cause first.
2. **In async contexts, `actor` is the default for shared mutable state.** In synchronous contexts, prefer refactoring to async and using an `actor`; reach for `Mutex` (from `Synchronization`) only when async refactoring is not feasible, the critical section has no `await`, or fine-grained locking over a single value is needed.
3. **Use `@MainActor` for all UI-bound state.** Apply it to `ObservableObject` view models and `@Observable` types that drive the view hierarchy.
4. **Prefer structured concurrency** (`async let`, `TaskGroup`) over unstructured `Task { }`. Attach work to a parent task whenever possible.
5. **Mark types `Sendable` explicitly** when they cross isolation boundaries. Prefer value types (`struct`, `enum`) — they are implicitly `Sendable` when all stored properties are `Sendable`.
6. **Propagate task cancellation.** Check `Task.isCancelled` or call `try Task.checkCancellation()` inside loops or long-running operations.
7. **Avoid `@unchecked Sendable`** unless integrating a third-party type that is thread-safe but lacks annotation. Always add a comment explaining why it is safe.
8. **Use `@preconcurrency import`** when adopting a framework not yet fully annotated for Swift 6, to suppress false-positive warnings at the boundary.
9. **No `DispatchQueue` in new Swift 6 code.** Use `actor` for shared mutable state. `Mutex` is acceptable for short synchronous critical sections, but never as a default replacement for actors.
10. **Never call `MainActor.assumeIsolated`** outside of truly non-async contexts (e.g., UIKit delegate callbacks). In async code, just annotate or `await MainActor.run { }`.

## Quick Triage

Before writing or reviewing concurrency code, clarify:

1. **Which isolation domain?** Main actor (UI), custom actor (domain/data service), or nonisolated?
2. **What mutability pattern?** Value type passed by copy, reference type shared by reference, or actor-protected state?
3. **Structured or unstructured?** Is there a parent `async` context to attach the child task to?
4. **Concurrency mode?** Check `SWIFT_STRICT_CONCURRENCY` in build settings — `complete` is Swift 6 mode.

## Canonical Patterns

### Actor protecting shared mutable state

```swift
actor TokenStore {

    private var accessToken: String?
    private var refreshToken: String?

    func store(access: String, refresh: String) {
        accessToken = access
        refreshToken = refresh
    }

    func retrieveAccessToken() -> String? {
        accessToken
    }
}
```

### `@MainActor` view model

```swift
@MainActor
final class HomeViewModel: ObservableObject {

    @Published private(set) var items: [HomeItem] = []
    @Published private(set) var isLoading = false

    private let repository: any HomeRepository

    init(repository: some HomeRepository) {
        self.repository = repository
    }

    func load() async {
        isLoading = true
        defer { isLoading = false }
        do {
            items = try await repository.fetchItems()
        } catch {
            items = []
        }
    }
}
```

### Structured concurrency with `async let`

```swift
func loadDashboard() async throws -> Dashboard {
    async let user = userRepository.fetchCurrentUser()
    async let orders = orderRepository.fetchRecentOrders()
    async let banners = bannerRepository.fetchActiveBanners()

    return try await Dashboard(
        user: user,
        orders: orders,
        banners: banners
    )
}
```

### `TaskGroup` for dynamic fan-out

```swift
func fetchDetails(for ids: [String]) async throws -> [ItemDetail] {
    try await withThrowingTaskGroup(of: ItemDetail.self) { group in
        for id in ids {
            group.addTask {
                try await self.repository.fetchDetail(id: id)
            }
        }
        return try await group.reduce(into: []) { $0.append($1) }
    }
}
```

### `Mutex` for synchronous shared state (no async)

```swift
import Synchronization

final class RequestCounter: Sendable {

    private let _count = Mutex(0)

    func increment() {
        _count.withLock { $0 += 1 }
    }

    var count: Int {
        _count.withLock { $0 }
    }
}
```

## Routing to References

Load the appropriate reference file based on the task:

| Task | Reference |
|------|-----------|
| Actor design, actor isolation, `nonisolated`, global actors | `references/actors.md` |
| `Sendable`, `@Sendable` closures, crossing isolation boundaries | `references/sendability.md` |
| `async let`, `Task`, `TaskGroup`, task cancellation | `references/structured-concurrency.md` |
| `AsyncSequence`, `AsyncStream`, `for await` loops | `references/async-sequences.md` |

## Verification Checklist

Before finishing, confirm:

- [ ] All mutable state shared across concurrency domains is actor-isolated or `Sendable`
- [ ] `@MainActor` is applied to all `ObservableObject` / `@Observable` view models
- [ ] `async let` or `TaskGroup` used instead of a chain of sequential `await` calls where parallelism is possible
- [ ] No `Task.detached` unless genuinely required — if used, explain why in a comment
- [ ] Long loops call `try Task.checkCancellation()` at each iteration
- [ ] `Mutex` only used when the critical section is short, purely synchronous, and has no `await` — actor is the default otherwise
- [ ] `AsyncStream` / `AsyncThrowingStream` continuations call `.finish()` in all exit paths including errors
- [ ] No `DispatchQueue` in new code
- [ ] `nonisolated(unsafe)` absent (or signed off by the user with a safety comment)
