Swift Concurrency Pro Agent Skill

repository·main·Indexed 19 days ago

https://github.com/twostraws/swift-concurrency-agent-skill

An agent skill for AI coding assistants (Claude Code, Cursor, Gemini, Codex) designed to help write safe, efficient Swift concurrency code. It covers advanced features and edge cases for Swift 6.2+ and iOS 26+, including async/await, Actors, Sendable, Task groups, @concurrent, and structured concurrency. The skill provides guidance on avoiding actor reentrancy bugs, protecting global state, managing AsyncStream lifecycles, and wrapping callback-based APIs using checked continuations.

Tokens
17.3K
Snippets
43
Records
76
Agent score
67%

What's inside Swift Concurrency Pro

  1. Overview of Swift Concurrency Pro

    main

    Swift Concurrency Pro is an agent skill designed to help AI coding assistants write high-quality Swift concurrency code. It targets common LLM mistakes and covers advanced or recent features that models might not be fully trained on.

    Key areas covered:

    • async/await
    • Actors
    • Sendable
    • Task groups
    • @concurrent
    • Structured concurrency

    Target Platforms:

    • iOS 26+
    • Swift 6.2+
  2. Use the swift-concurrency-pro agent skill

    main

    The swift-concurrency-pro skill is designed to review Swift code for concurrency correctness, modern API usage, and common async/await pitfalls. It is specifically optimized for Swift 6.2 or later with strict concurrency checking enabled.

    How to use

    When invoking the skill, you can provide an optional [focus area] argument to narrow the scope of the review.

    Core Review Principles

    • Target Version: Focuses on Swift 6.2+ with strict concurrency.
    • Structured Concurrency: Prefers task groups over unstructured Task {} blocks.
    • Modern APIs: Prefers async/await over closure-based variants and Swift concurrency over Grand Central Dispatch (GCD).
    • Safety over Silencing: Never suggests @unchecked Sendable to fix compiler errors; instead, it recommends actors, value types, or sending parameters.
    • Context Awareness: Compares concurrency build settings across different targets or packages if the code spans multiple modules.
    argument-hint: "[focus area]"
  3. Choose between `Task {}` and `Task.detached {}`

    main

    When using unstructured concurrency, understand the difference in actor isolation:

    • Task {}: Inherits the caller's actor isolation. If called from a @MainActor function, the task body runs on the @MainActor, making it safe to perform UI updates.
    • Task.detached {}: Does not inherit the caller's actor isolation or priority. It is used for genuinely independent background work that needs to shed the current actor context.

    Best Practice: Prefer Task {} with explicit isolation changes or structured concurrency. Only use Task.detached when you specifically need to shed the caller's actor context and priority.

    @MainActor
    func example() {
        Task {
            // Still on MainActor; safe to update UI here.
            label.text = "Done"
        }
    
        Task.detached {
            // Not on MainActor; updating UI here is a bug.
            // Use this for genuinely independent background work.
        }
    }
  4. Avoid actor reentrancy bugs

    main

    A common concurrency bug occurs when assuming an actor's state remains unchanged after an await call. Because actors are reentrant, other tasks may have modified the actor's state during the suspension point.

    Best Practices:

    1. Never assume state is unchanged after await.
    2. Capture results in local variables: Instead of force-unwrapping an actor property after an await, assign the result of the async work to a local variable first, then update the actor state.
    3. Manage in-flight tasks: To prevent multiple callers from performing the same expensive work (like downloading a file) simultaneously, store the Task in a dictionary (e.g., inFlight) and have subsequent callers await the existing task.
    // Fix: Capture the result in a local, then assign to avoid reentrancy issues.
    actor VideoCache {
        var items: [URL: Video] = [: ]
    
        func video(for url: URL) async throws -> Video {
            if let cached = items[url] { return cached }
            let video = try await downloadVideo(url)
            items[url] = video
            return video
        }
    }
    
    // Advanced: Use an in-flight dictionary to prevent duplicate work
    actor VideoCache {
        var items: [URL: Video] = [:]
        var inFlight: [URL: Task<Video, Error>] = [:]
    
        func video(for url: URL) async throws -> Video {
            if let cached = items[url] { return cached }
    
            if let task = inFlight[url] {
                return try await task.value
            }
    
            let task = Task {
                try await downloadVideo(url)
            }
    
            inFlight[url] = task
    
            do {
                let video = try await task.value
                items[url] = video
                inFlight[url] = nil
                return video
            } catch {
                inFlight[url] = nil
                throw error
            }
        }
    }
  5. Core principles for Swift concurrency reviews

    main

    When performing reviews with this skill, the following technical constraints and preferences apply:

    • Target Version: Swift 6.2 or later with strict concurrency checking enabled.
    • Concurrency Model: Prefer Swift Concurrency over Grand Central Dispatch (GCD). GCD is acceptable for low-level, performance-critical synchronous work or framework interop, but should not be the default for new code.
    • Structured vs Unstructured: Always prefer structured concurrency (e.g., TaskGroup) over unstructured tasks (Task {}).
    • API Preference: If an API provides both async/await and closure-based variants, always prefer the async/await version.
    • Avoid @unchecked Sendable: Do not suggest @unchecked Sendable to silence compiler errors. Instead, use actors, value types, or sending parameters to fix the underlying race condition. @unchecked Sendable should only be used for types with internal locking that are provably thread-safe.
    • Build Settings: If code spans multiple targets or packages, compare their concurrency build settings to ensure consistent behavior.
  6. Specify types for task groups

    main

    While Swift can often infer the type of a task group for simple types (like String, URL, or Data), you must explicitly provide the type using the of: parameter when the return type is complex, such as a tuple containing a Result type.

    // Explicitly specifying the type for a complex return value
    await withTaskGroup(of: (URL, Result<Data, Error>).self) { group in
        // ...
    }
  7. How cancellation propagates in Swift concurrency

    main

    Cancellation in Swift is cooperative, meaning setting a cancellation flag does not automatically stop execution; the running code must actively check for it. Propagation follows these rules:

    • Structured Concurrency: Cancelling a parent task automatically cancels all its child tasks. This includes tasks within a TaskGroup.
    • Unstructured Concurrency: Task {} and Task.detached {} do not inherit cancellation from a parent. You must manually store the task handle and call .cancel() on it.
    • SwiftUI: The .task() view modifier is the preferred way to run async work in views because it automatically cancels the task when the view disappears from the hierarchy.
  8. Choosing between `async let` and task groups

    main

    When implementing concurrency in Swift, choose your tool based on the nature of the operations:

    • Use async let: When you have a fixed number of independent operations that return different types (e.g., fetching news, weather, and app updates simultaneously).
    • Use Task Groups: When you have a dynamic number of operations that return the same type (e.g., downloading a list of images from an array of URLs).
  9. Understand `nonisolated` async function execution behavior

    main

    In Swift 6.2, the mental model for nonisolated async functions has changed: a nonisolated async function now stays on the caller's actor by default unless it is explicitly offloaded elsewhere.

    Implications:

    • Calling a nonisolated async method on a helper struct no longer implies automatic background execution.
    • If you require the function to run on the concurrent pool (background) rather than the caller's actor, you must use explicit offloading (such as the @concurrent attribute).
    struct Measurements {
        func fetchLatest() async throws -> [Double] {
            let url = URL(string: "https://hws.dev/readings.json")!
            let (data, _) = try await URLSession.shared.data(from: url)
            return try JSONDecoder().decode([Double].self, from: data)
        }
    }
    
    @MainActor
    struct WeatherStation {
        let measurements = Measurements()
    
        func getAverageTemperature() async throws -> Double {
            // In Swift 6.2, this call stays on the @MainActor
            let readings = try await measurements.fetchLatest()
            return readings.reduce(0, +) / Double(readings.count)
        }
    }
  10. Use the `.serialized` trait for parameterized tests

    main

    By default, Swift Testing runs tests in parallel. If you need to control the execution order of parameterized tests, use the .serialized trait.

    Important Constraints:

    • .serialized only affects parameterized tests (tests using arguments:). It tells Swift Testing to run each argument case one at a time rather than in parallel.
    • Applying .serialized to a non-parameterized test has no effect.
    • Applying it to a whole suite only serializes the parameterized tests within that suite; other tests in the suite remain parallel.
    // .serialized controls execution order of parameterized cases only.
    @Test(.serialized, arguments: ["alice", "bob", "charlie"])
    func accountCreation(username: String) async throws {
        let account = try await AccountService().create(username: username)
        #expect(account.isActive)
    }
  11. Handle actor isolation in callback code

    main

    Callback-based APIs often lack explicit actor isolation in their type signatures, which can lead to runtime failures in Swift 6.

    • Runtime Traps: If a callback attempts to access MainActor state without a type-system guarantee, Swift 6 runtime checks may trap (crash) to prevent silent data races.
    • Manual Isolation: Use MainActor.assumeIsolated() only when you have a verified guarantee that the callback is running on the main actor, but the compiler is unable to see that guarantee through the type system.