--- name: swift-concurrency description: Expert guidance on Swift Concurrency concepts. Use when working with async/await, Tasks, actors, MainActor, Sendable, isolation domains, or debugging concurrency compiler errors. Helps write safe concurrent Swift code. --- # Swift Concurrency Skill This skill provides expert guidance on Swift's concurrency system based on the mental models from [Fucking Approachable Swift Concurrency](https://fuckingapproachableswiftconcurrency.com). ## Core Mental Model: The Office Building Think of your app as an office building where **isolation domains** are private offices with locks: - **MainActor** = Front desk (handles all UI interactions, only one exists) - **actor** types = Department offices (Accounting, Legal, HR - each protects its own data) - **nonisolated** code = Hallways (shared space, no private documents) - **Sendable** types = Photocopies (safe to share between offices) - **Non-Sendable** types = Original documents (must stay in one office) You can't barge into someone's office. You knock (`await`) and wait. ## Async/Await An `async` function can pause. Use `await` to suspend until work finishes: ```swift func fetchUser(id: Int) async throws -> User { let (data, _) = try await URLSession.shared.data(from: url) return try JSONDecoder().decode(User.self, from: data) } ``` For parallel work, use `async let`: ```swift async let avatar = fetchImage("avatar.jpg") async let banner = fetchImage("banner.jpg") return Profile(avatar: try await avatar, banner: try await banner) ``` ## Tasks A `Task` is a unit of async work you can manage: ```swift // SwiftUI - cancels when view disappears .task { avatar = await downloadAvatar() } // Manual task creation Task { await saveProfile() } // Parallel work with TaskGroup try await withThrowingTaskGroup(of: Void.self) { group in group.addTask { avatar = try await downloadAvatar() } group.addTask { bio = try await fetchBio() } try await group.waitForAll() } ``` Child tasks in a group: cancellation propagates, errors cancel siblings, waits for all to complete. ## Isolation Domains Swift asks "who can access this data?" not "which thread?". Three isolation domains: ### 1. MainActor For UI. Everything UI-related should be here: ```swift @MainActor class ViewModel { var items: [Item] = [] // Protected by MainActor } ``` ### 2. Actors Protect their own mutable state with exclusive access: ```swift actor BankAccount { var balance: Double = 0 func deposit(_ amount: Double) { balance += amount } } await account.deposit(100) // Must await from outside ``` ### 3. Nonisolated Opts out of actor isolation. Cannot access actor's protected state: ```swift actor BankAccount { nonisolated func bankName() -> String { "Acme Bank" } } let name = account.bankName() // No await needed ``` ## Approachable Concurrency (Swift 6.2+) Two build settings that simplify the mental model: - **SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor**: Everything runs on MainActor unless you say otherwise - **SWIFT_APPROACHABLE_CONCURRENCY = YES**: nonisolated async functions stay on caller's actor ```swift // Runs on MainActor (default) func updateUI() async { } // Runs on background (opt-in) @concurrent func processLargeFile() async { } ``` ## Sendable Marks types safe to pass across isolation boundaries: ```swift // Sendable - value type, each gets a copy struct User: Sendable { let id: Int let name: String } // Non-Sendable - mutable class state class Counter { var count = 0 } ``` Automatically Sendable: - Structs/enums with only Sendable properties - Actors (protect their own state) - @MainActor types (MainActor serializes access) For thread-safe classes with internal synchronization: ```swift final class ThreadSafeCache: @unchecked Sendable { private let lock = NSLock() private var storage: [String: Data] = [:] } ``` ## Isolation Inheritance With Approachable Concurrency, isolation flows from MainActor through your code: - **Functions**: Inherit caller's isolation unless explicitly marked - **Closures**: Inherit from context where defined - **Task { }**: Inherits actor isolation from creation site - **Task.detached { }**: No inheritance (rarely needed) ### Preserving Isolation in Async Utilities When writing generic async functions that accept closures, you need to preserve the caller's isolation to avoid Sendable errors. **Option 1: `nonisolated(nonsending)`** (simpler) ```swift // Stays on caller's executor, no Sendable needed nonisolated(nonsending) func measure(_ label: String, block: () async throws -> T) async rethrows -> T ``` **Option 2: `#isolation` parameter** (when you need actor access) ```swift // Explicit isolation parameter, useful if you need to pass it around func measure( isolation: isolated (any Actor)? = #isolation, _ label: String, block: () async throws -> T ) async rethrows -> T ``` Use `nonisolated(nonsending)` by default. Use `#isolation` when you need explicit access to the actor instance. ## Common Mistakes to Avoid ### 1. Thinking async = background ```swift // Still blocks main thread! @MainActor func slowFunction() async { let result = expensiveCalculation() // Synchronous = blocking } // Fix: Use @concurrent for CPU-heavy work ``` ### 2. Creating too many actors Most things can live on MainActor. Only create actors when you have shared mutable state that can't be on MainActor. ### 3. Making everything Sendable Not everything needs to cross boundaries. Step back and ask if data actually moves between isolation domains. ### 4. Using MainActor.run unnecessarily ```swift // Unnecessary await MainActor.run { self.data = data } // Better - annotate the function @MainActor func loadData() async { self.data = await fetchData() } ``` ### 5. Blocking the cooperative thread pool Never use DispatchSemaphore, DispatchGroup.wait() in async code. Risks deadlock. ### 6. Creating unnecessary Tasks ```swift // Bad - unstructured Task { await fetchUsers() } Task { await fetchPosts() } // Good - structured concurrency async let users = fetchUsers() async let posts = fetchPosts() await (users, posts) ``` ## Quick Reference | Keyword | Purpose | |---------|---------| | `async` | Function can pause | | `await` | Pause here until done | | `Task { }` | Start async work, inherits context | | `Task.detached { }` | Start async work, no context | | `@MainActor` | Runs on main thread | | `actor` | Type with isolated mutable state | | `nonisolated` | Opts out of actor isolation | | `nonisolated(nonsending)` | Stay on caller's executor | | `Sendable` | Safe to pass between isolation domains | | `@concurrent` | Always run on background (Swift 6.2+) | | `#isolation` | Capture caller's isolation as parameter | | `async let` | Start parallel work | | `TaskGroup` | Dynamic parallel work | ## When the Compiler Complains Trace the isolation: Where did it come from? Where is code trying to run? What data crosses a boundary? The answer is usually obvious once you ask the right question. ## Further Reading - [Matt Massicotte's Blog](https://www.massicotte.org/) - The source of these mental models - [Swift Concurrency Documentation](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/) - [WWDC21: Meet async/await](https://developer.apple.com/videos/play/wwdc2021/10132/) - [WWDC21: Protect mutable state with actors](https://developer.apple.com/videos/play/wwdc2021/10133/)