Claude Code Plugins

Community-maintained marketplace

Feedback

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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.

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:

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:

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:

// 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:

@MainActor
class ViewModel {
    var items: [Item] = []  // Protected by MainActor
}

2. Actors

Protect their own mutable state with exclusive access:

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:

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
// 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:

// 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:

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)

Common Mistakes to Avoid

1. Thinking async = background

// 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

// 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

// 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
Sendable Safe to pass between isolation domains
@concurrent Always run on background (Swift 6.2+)
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