swift-concurrency

Master Swift concurrency - async/await, actors, structured concurrency, Sendable, TaskGroups

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "swift-concurrency" with this command: npx skills add pluginagentmarketplace/custom-plugin-swift/pluginagentmarketplace-custom-plugin-swift-swift-concurrency

Swift Concurrency Skill

Modern Swift concurrency patterns using async/await, actors, and structured concurrency.

Prerequisites

  • Swift 5.5+ / iOS 15+ / macOS 12+
  • Understanding of threading concepts
  • Familiarity with closures

Parameters

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

Topics Covered

Core Concepts

ConceptPurpose
async/awaitSequential async code
ActorData isolation
TaskUnit of async work
SendableThread-safe types
MainActorMain thread isolation

Task Types

TypeLifetimeCancellation
Task {}IndependentManual
Task.detached {}No context inheritedManual
async letStructuredAutomatic
TaskGroupStructured, multipleAutomatic

Actor Isolation

AnnotationMeaning
actorType is an actor
@MainActorRuns on main thread
nonisolatedOpt out of isolation
isolatedParameter isolation

Code Examples

Basic async/await

// 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 for Thread Safety

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"
    }
}

TaskGroup for Parallel Operations

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 for UI

@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
    }
}

Sendable Conformance

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

Cancellation Handling

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

Troubleshooting

Common Issues

IssueCauseSolution
"Actor-isolated property cannot be accessed"Cross-actor accessUse await or nonisolated
"Capture of non-sendable type"Non-Sendable in closureMake type Sendable or use actor
"Reference to captured var in concurrently-executing code"Mutable captureUse let or actor
Task hangsMissing awaitAdd await to all async calls
DeadlockActor calling itselfUse nonisolated for pure functions

Debug Tips

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

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

Usage

Skill("swift-concurrency")

Related Skills

  • swift-fundamentals - Language basics
  • swift-combine - Reactive alternative
  • swift-testing - Testing async code

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

swift-uikit

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-macos

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-architecture

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

swift-spm

No summary provided by upstream source.

Repository SourceNeeds Review