一键导入
swift-concurrency-developer
// Expert guidance on Swift concurrency using the Office Building mental model. Use when working with actors, isolation, Sendable, TaskGroups, or fixing concurrency warnings and data race issues.
// Expert guidance on Swift concurrency using the Office Building mental model. Use when working with actors, isolation, Sendable, TaskGroups, or fixing concurrency warnings and data race issues.
Triage Sentry crashes for an iOS release. Pulls unresolved fatal events from sentry.anytype.io, investigates each fingerprint cluster against the source, creates one Linear ticket per cluster with a root-cause hypothesis (no proposed fix - the implementer figures that out with full context), then archives the Sentry issue (status `ignored`) so it stops cluttering the inbox. Activate on "triage Sentry crashes", "triage fatal errors", "investigate crashes in release", "fatal errors in 0.X.Y", or any time the user wants to turn a release's Sentry inbox into actionable Linear tickets. The slash entry is `/do-sentry-triage`.
Context-aware routing to iOS 26 Liquid Glass implementation patterns. Use when working with glass effects, GlassEffectContainer, morphing transitions, or iOS 26 visual effects.
Decompose a large Linear task into independent subtasks with a master plan. Analyzes issue, project, related tasks, PRs, and codebase to create PLAN.md and Linear sub-issues.
Context-aware routing to Swift/iOS development patterns, architecture, and best practices. Use when working with .swift files, ViewModels, Coordinators, refactoring, or discussing Swift/SwiftUI patterns.
SwiftUI view structure, composition, and best practices. Use when refactoring SwiftUI views, organizing view files, or extracting subviews.
Audit and improve SwiftUI runtime performance through code review and Instruments guidance. Use for diagnosing slow rendering, janky scrolling, excessive view updates, or layout thrash in SwiftUI apps.
| name | swift-concurrency-developer |
| description | Expert guidance on Swift concurrency using the Office Building mental model. Use when working with actors, isolation, Sendable, TaskGroups, or fixing concurrency warnings and data race issues. |
Expert guidance on Swift's concurrency system using the "Office Building" mental model from Fucking Approachable Swift Concurrency, combined with comprehensive reference material from Swift Concurrency Course.
actor, isolation, Sendable, TaskGroup, nonisolated, async let@MainActor, custom actor, actor instance isolation, or nonisolated.@MainActor as a blanket fix. Justify why main-actor isolation is correct for the code.Task.detached only with a clear reason.@preconcurrency, @unchecked Sendable, or nonisolated(unsafe), require:
Always confirm these before interpreting diagnostics or giving migration-sensitive guidance. Do not guess — if any are unknown, ask the developer.
| Setting | SwiftPM (Package.swift) | Xcode (.pbxproj) |
|---|---|---|
| Language mode | .swiftLanguageMode(.v6) per-target inside swiftSettings (NOT package-level swiftLanguageVersions, which only advertises compatibility) | SWIFT_VERSION |
| Strict concurrency | .enableExperimentalFeature("StrictConcurrency=targeted") | SWIFT_STRICT_CONCURRENCY |
| Default isolation | .defaultIsolation(MainActor.self) | SWIFT_DEFAULT_ACTOR_ISOLATION |
| Upcoming features | .enableUpcomingFeature("NonisolatedNonsendingByDefault") | SWIFT_UPCOMING_FEATURE_* |
Tools: Read on Package.swift, Grep on .pbxproj.
Think of your app as an office building where isolation domains are private offices with locks:
| Concept | Office Analogy | Swift |
|---|---|---|
| MainActor | Front desk (handles all UI) | @MainActor |
| actor | Department offices (Accounting, Legal) | actor BankAccount { } |
| nonisolated | Hallways (shared space) | nonisolated func name() |
| Sendable | Photocopies (safe to share) | struct User: Sendable |
| Non-Sendable | Original documents (stay in one office) | class Counter { } |
Key insight: You can't barge into someone's office. You knock (await) and wait.
When a developer needs concurrency guidance:
Starting fresh with async code?
references/async-await-basics.md for foundational patternsreferences/tasks.md (async let, task groups)Protecting shared mutable state?
references/actors.md (actors, @MainActor)references/sendable.md (Sendable conformance)Managing async operations?
references/tasks.md (Task, child tasks, cancellation)references/async-sequences.md (AsyncSequence, AsyncStream)Working with legacy frameworks?
references/core-data.mdreferences/migration.mdPerformance or debugging issues?
references/performance.md (profiling, suspension points)references/testing.md (XCTest, Swift Testing)Understanding threading behavior?
references/threading.md for thread/task relationship and isolationMemory issues with tasks?
references/memory-management.md for retain cycle preventionreferences/linting.md for rule intent and preferred fixes; avoid dummy awaits as "fixes".references/sendable.md and references/threading.md@MainActorreferences/actors.md (global actors, nonisolated, isolated parameters)references/testing.md (await fulfillment(of:) and Swift Testing patterns)references/core-data.md (DAO/NSManagedObjectID, default isolation conflicts)func fetchUser(id: Int) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
async let avatar = fetchImage("avatar.jpg")
async let banner = fetchImage("banner.jpg")
return Profile(avatar: try await avatar, banner: try await banner)
// SwiftUI - cancels when view disappears
.task { avatar = await downloadAvatar() }
// Manual task (inherits actor context)
Task { await saveProfile() }
Match a Task's entry isolation to its synchronous prefix — everything from { to the first await. Whatever runs in that prefix executes on the inherited actor.
@MainActor (touches UI state, mutates self.isLoading, etc.), keep the inherited start.Task { @concurrent in ... } and hop back via MainActor.run { ... } only for the UI mutation.print) followed by main-actor work is not a reason to switch to @concurrent — the cheap line rides along.// ❌ Called from @MainActor; fetchData() is nonisolated, so the task starts on main then hops away
// (whether the hop happens depends on fetchData()'s declared isolation — nonisolated/@concurrent hop, @MainActor does not)
Task {
await fetchData() // nonisolated async
}
// ✅ Start off the main actor, hop back only for UI work
Task { @concurrent in
let data = try await fetchData()
await MainActor.run { self.items = data }
}
// ✅ Prefix DOES need main actor — keep inheritance
Task {
self.isLoading = true // needs @MainActor, before any await
await fetchData()
self.isLoading = false
}
try await withThrowingTaskGroup(of: Void.self) { group in
group.addTask { avatar = try await downloadAvatar() }
group.addTask { bio = try await fetchBio() }
try await group.waitForAll()
}
actor BankAccount {
var balance: Double = 0
func deposit(_ amount: Double) { balance += amount }
// No await needed - can access directly inside actor
nonisolated func bankName() -> String { "Acme Bank" }
}
await account.deposit(100) // Must await from outside
let name = account.bankName() // No await needed
// Automatically Sendable - value type
struct User: Sendable {
let id: Int
let name: String
}
// Thread-safe class with internal synchronization
final class ThreadSafeCache: @unchecked Sendable {
private let lock = NSLock()
private var storage: [String: Data] = [:]
}
// WRONG: Still blocks main thread!
@MainActor func slowFunction() async {
let result = expensiveCalculation() // Synchronous = blocking
}
// CORRECT: Use detached task for CPU-heavy work
Task.detached(priority: .userInitiated) {
let result = expensiveCalculation()
await MainActor.run { updateUI(result) }
}
Production impact: Apps get rejected for "became unresponsive." See
references/production-pitfalls.mdsection 2.
Most things can live on MainActor. Only create actors when you have shared mutable state that can't be on MainActor.
// WRONG
await MainActor.run { self.data = data }
// CORRECT - annotate the function
@MainActor func loadData() async { self.data = await fetchData() }
Never use DispatchSemaphore, DispatchGroup.wait(), or condition variables in async code.
Why: These primitives hide dependencies from the runtime. The cooperative thread pool has a contract that threads will always make forward progress. Blocking primitives violate this contract and can cause deadlock.
// ❌ DANGEROUS: Can deadlock the cooperative pool
let semaphore = DispatchSemaphore(value: 0)
Task {
await doWork()
semaphore.signal()
}
semaphore.wait() // Thread blocked, runtime unaware
// ✅ Use async/await instead
let result = await doWork()
Debug tip: Set LIBDISPATCH_COOPERATIVE_POOL_STRICT=1 to catch blocking calls during development.
// WRONG - unstructured
Task { await fetchUsers() }
Task { await fetchPosts() }
// CORRECT - structured concurrency
async let users = fetchUsers()
async let posts = fetchPosts()
await (users, posts)
Not everything needs to cross boundaries. Ask if data actually moves between isolation domains.
The main thread is separate from the cooperative thread pool. Each hop to/from MainActor requires a full context switch.
// ❌ Multiple context switches
for item in items {
let processed = await processItem(item)
await MainActor.run { displayItem(processed) } // Context switch per item
}
// ✅ Single context switch
let processed = await processAllItems(items)
await MainActor.run {
for item in processed { displayItem(item) }
}
Using try? or empty catch {} in async loops swallows failures. Users lose data with zero indication. Acceptable for fire-and-forget (cache warming, analytics), dangerous for uploads/sync/migration. See references/production-pitfalls.md section 1.
for await under .task modifier is safe (structured concurrency propagates cancellation). But for await or while loops in stored Task { } properties need explicit Task.isCancelled checks. See references/production-pitfalls.md section 3.
@preconcurrency and nonisolated(unsafe) hide real data races. Mixing DispatchQueue with async/await creates confusing execution contexts. Always document safety invariants and plan removal. See references/production-pitfalls.md section 4.
Task { } in .onAppear is unstructured: not cancelled on disappear, fires on every re-appear. Use .task { } for async work tied to view lifecycle, .onAppear for sync-only setup. See references/production-pitfalls.md section 5.
| 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 |
@unchecked Sendable | Trust me, it's thread-safe |
async let | Start parallel work |
TaskGroup | Dynamic parallel work |
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.
Load these files as needed for specific topics:
async-await-basics.md - async/await syntax, execution order, async let, URLSession patternstasks.md - Task lifecycle, cancellation, priorities, task groups, structured vs unstructuredthreading.md - Thread/task relationship, suspension points, isolation domains, nonisolatedglossary.md - Quick definitions of core concurrency termsactors.md - Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutexsendable.md - Sendable conformance, value/reference types, @unchecked, region isolationmemory-management.md - Retain cycles in tasks, memory safety patternsasync-sequences.md - AsyncSequence, AsyncStream, when to use vs regular async methodsasync-algorithms.md - Swift Async Algorithms package, combining sequencestask-local-values.md - Task-local context propagation, tracing, logging patternscore-data.md - NSManagedObject sendability, custom executors, isolation conflictsperformance.md - Profiling with Instruments, reducing suspension points, execution strategiestesting.md - XCTest async patterns, Swift Testing, concurrency testing utilitiesmigration.md - Swift 6 migration strategy, closure-to-async conversion, @preconcurrencylinting.md - Concurrency-focused lint rules and SwiftLint async_without_awaitproduction-pitfalls.md - Silent data loss, cancellation gaps, migration bridges, .task vs onAppearapproachable-concurrency.md - Approachable concurrency quick guideswift-6-2-concurrency.md - Swift 6.2 concurrency updates (future reference)swiftui-concurrency-tour.md - SwiftUI-specific concurrency patternsreferences/testing.md).references/performance.md).references/memory-management.md).For Swift 6 / strict-concurrency migration, apply this cycle for each change:
Never batch unrelated fixes into one change. If a fix introduces new warnings, resolve them before continuing. Keep commits small and reviewable. See references/migration.md for detailed migration steps.
IOS_DEVELOPMENT_GUIDE.md - General Swift/iOS patterns, MVVM, CoordinatorsNavigation: This skill provides concurrency mental models. For general Swift/iOS patterns, see ios-dev-guidelines.
Attribution: Office Building mental model from Dimillian/Skills. Reference files from AvdLee/Swift-Concurrency-Agent-Skill.