Master Swift concurrency - async/await, actors, structured concurrency, Sendable, TaskGroups
Provides Swift concurrency patterns (async/await, actors, TaskGroups) for thread-safe async code. Use when writing async operations, managing actor isolation, or handling parallel tasks in Swift projects.
/plugin marketplace add pluginagentmarketplace/custom-plugin-swift/plugin install swift-assistant@pluginagentmarketplace-swiftThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/config.yamlreferences/GUIDE.mdscripts/check-sendable.shModern Swift concurrency patterns using async/await, actors, and structured concurrency.
parameters:
strict_concurrency:
type: string
enum: [minimal, targeted, complete]
default: complete
description: Concurrency checking level
actor_isolation:
type: boolean
default: true
use_main_actor:
type: boolean
default: true
description: MainActor for UI code
| Concept | Purpose |
|---|---|
| async/await | Sequential async code |
| Actor | Data isolation |
| Task | Unit of async work |
| Sendable | Thread-safe types |
| MainActor | Main thread isolation |
| Type | Lifetime | Cancellation |
|---|---|---|
| Task {} | Independent | Manual |
| Task.detached {} | No context inherited | Manual |
| async let | Structured | Automatic |
| TaskGroup | Structured, multiple | Automatic |
| Annotation | Meaning |
|---|---|
actor | Type is an actor |
@MainActor | Runs on main thread |
nonisolated | Opt out of isolation |
isolated | Parameter isolation |
// Sequential async operations
func fetchUserProfile(userId: String) async throws -> UserProfile {
let user = try await api.fetchUser(userId)
let posts = try await api.fetchPosts(userId: userId)
let followers = try await api.fetchFollowers(userId: userId)
return UserProfile(user: user, posts: posts, followers: followers)
}
// Concurrent with async let
func fetchUserProfileConcurrently(userId: String) async throws -> UserProfile {
async let user = api.fetchUser(userId)
async let posts = api.fetchPosts(userId: userId)
async let followers = api.fetchFollowers(userId: userId)
// All three run concurrently, await collects results
return try await UserProfile(user: user, posts: posts, followers: followers)
}
actor ImageCache {
private var cache: [URL: UIImage] = [:]
private var inProgress: [URL: Task<UIImage, Error>] = [:]
func image(for url: URL) async throws -> UIImage {
// Return cached
if let cached = cache[url] {
return cached
}
// Return in-progress task (avoid duplicate downloads)
if let existing = inProgress[url] {
return try await existing.value
}
// Start new download
let task = Task {
let (data, _) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else {
throw ImageError.invalidData
}
return image
}
inProgress[url] = task
do {
let image = try await task.value
cache[url] = image
inProgress[url] = nil
return image
} catch {
inProgress[url] = nil
throw error
}
}
func clearCache() {
cache.removeAll()
}
// Nonisolated for synchronous read
nonisolated var cacheDescription: String {
"ImageCache instance"
}
}
func fetchAllProducts(ids: [String]) async throws -> [Product] {
try await withThrowingTaskGroup(of: Product.self) { group in
for id in ids {
group.addTask {
try await self.api.fetchProduct(id: id)
}
}
var products: [Product] = []
for try await product in group {
products.append(product)
}
return products
}
}
// With concurrency limit
func fetchWithLimit(ids: [String], maxConcurrent: Int = 4) async throws -> [Product] {
try await withThrowingTaskGroup(of: Product.self) { group in
var iterator = ids.makeIterator()
var products: [Product] = []
// Start initial batch
for _ in 0..<min(maxConcurrent, ids.count) {
if let id = iterator.next() {
group.addTask { try await self.api.fetchProduct(id: id) }
}
}
// As each completes, add another
for try await product in group {
products.append(product)
if let id = iterator.next() {
group.addTask { try await self.api.fetchProduct(id: id) }
}
}
return products
}
}
@MainActor
final class ProductListViewModel: ObservableObject {
@Published private(set) var products: [Product] = []
@Published private(set) var isLoading = false
@Published private(set) var error: Error?
private let repository: ProductRepository
init(repository: ProductRepository) {
self.repository = repository
}
func loadProducts() async {
isLoading = true
error = nil
do {
products = try await repository.fetchProducts()
} catch {
self.error = error
}
isLoading = false
}
// Nonisolated for non-UI work
nonisolated func precomputeHash(for product: Product) -> Int {
product.hashValue
}
}
// Value types are Sendable automatically if properties are
struct Product: Sendable {
let id: String
let name: String
let price: Decimal
}
// Classes need explicit conformance
final class ProductCache: @unchecked Sendable {
private let lock = NSLock()
private var cache: [String: Product] = [:]
func get(_ id: String) -> Product? {
lock.lock()
defer { lock.unlock() }
return cache[id]
}
func set(_ product: Product) {
lock.lock()
defer { lock.unlock() }
cache[product.id] = product
}
}
// Sendable closure
func process(_ items: [Item], transform: @Sendable (Item) -> Result) async -> [Result] {
await withTaskGroup(of: Result.self) { group in
for item in items {
group.addTask {
transform(item)
}
}
var results: [Result] = []
for await result in group {
results.append(result)
}
return results
}
}
func downloadLargeFile(url: URL) async throws -> Data {
var data = Data()
let (stream, response) = try await URLSession.shared.bytes(from: url)
let expectedLength = response.expectedContentLength
for try await byte in stream {
// Check for cancellation periodically
try Task.checkCancellation()
data.append(byte)
// Report progress (would need actor for thread safety)
let progress = Double(data.count) / Double(expectedLength)
await reportProgress(progress)
}
return data
}
// Usage with timeout
func downloadWithTimeout(url: URL, timeout: Duration) async throws -> Data {
try await withThrowingTaskGroup(of: Data.self) { group in
group.addTask {
try await self.downloadLargeFile(url: url)
}
group.addTask {
try await Task.sleep(for: timeout)
throw DownloadError.timeout
}
// First to complete wins, other is cancelled
let result = try await group.next()!
group.cancelAll()
return result
}
}
| Issue | Cause | Solution |
|---|---|---|
| "Actor-isolated property cannot be accessed" | Cross-actor access | Use await or nonisolated |
| "Capture of non-sendable type" | Non-Sendable in closure | Make type Sendable or use actor |
| "Reference to captured var in concurrently-executing code" | Mutable capture | Use let or actor |
| Task hangs | Missing await | Add await to all async calls |
| Deadlock | Actor calling itself | Use nonisolated for pure functions |
// Print current task priority
print("Priority: \(Task.currentPriority)")
// Check if cancelled
if Task.isCancelled {
return
}
// Add task-local values for debugging
enum RequestID: TaskLocalKey {
static var defaultValue: String? { nil }
}
extension Task where Success == Never, Failure == Never {
static var requestID: String? {
get { self[RequestID.self] }
set { self[RequestID.self] = newValue }
}
}
validation:
- rule: strict_concurrency
severity: error
check: Build with -strict-concurrency=complete
- rule: sendable_conformance
severity: warning
check: Types crossing actor boundaries must be Sendable
- rule: main_actor_ui
severity: error
check: UI updates must be on MainActor
Skill("swift-concurrency")
swift-fundamentals - Language basicsswift-combine - Reactive alternativeswift-testing - Testing async codeThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.