Agent Skills: Swift Concurrency Skill

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

swiftconcurrencyasync-awaitstructured-concurrencyactors
developmentID: pluginagentmarketplace/custom-plugin-swift/swift-concurrency

Skill Files

Browse the full folder contents for swift-concurrency.

Download Skill

Loading file tree…

skills/swift-concurrency/SKILL.md

Skill Metadata

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

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

| Concept | Purpose | |---------|---------| | async/await | Sequential async code | | Actor | Data isolation | | Task | Unit of async work | | Sendable | Thread-safe types | | MainActor | Main thread isolation |

Task Types

| Type | Lifetime | Cancellation | |------|----------|--------------| | Task {} | Independent | Manual | | Task.detached {} | No context inherited | Manual | | async let | Structured | Automatic | | TaskGroup | Structured, multiple | Automatic |

Actor Isolation

| Annotation | Meaning | |------------|---------| | actor | Type is an actor | | @MainActor | Runs on main thread | | nonisolated | Opt out of isolation | | isolated | Parameter 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

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

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