Swift Concurrency Agent Skill

repository·main·Indexed 23 days ago

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

Expert-level guidance for AI coding agents to help developers write safe, performant, and modern Swift code. This skill focuses on Swift 6 migration, data race prevention, and concurrency best practices, covering topics such as async/await, actors, Sendable conformance, and Core Data integration. It provides specialized documentation for handling actor reentrancy, @MainActor usage, and performance optimization.

Tokens
45K
Snippets
108
Records
167
Agent score
81%

What's inside swift-concurrency-agent-skill

  1. Navigate Swift Concurrency reference topics

    main

    The Swift Concurrency skill provides specialized documentation across several domains. Use the following categories to find specific guidance:

    Foundations

    • Async/Await Basics: Closure-to-async bridges and foundational usage.
    • Tasks: Task management, cancellation, task groups, and structured vs unstructured work.
    • Actors: Actor isolation, @MainActor, reentrancy, and isolated conformances.
    • Sendable: Sendable protocols, @Sendable closures, region isolation, and escape hatches.
    • Threading: Execution models, suspension points, and Swift 6.2 isolation behavior.

    Streams

    • Async Sequences: Deciding between AsyncSequence, AsyncStream, and one-shot async APIs.
    • Async Algorithms: Using operators like debounce, throttle, merge, combineLatest, channels, and timers.

    Applied Topics

    • Testing: Using Swift Testing (preferred) or XCTest, and performing leak checks.
    • Performance: Instruments workflow, analyzing actor hops, and suspension costs.
    • Memory Management: Handling retain cycles, long-lived tasks, and cleanup.
    • Core Data: Managing NSManagedObjectID, perform, and default isolation conflicts.

    Migration and Tooling

    • Migration: Rollout order, build settings, and migration guardrails.
    • Linting: Concurrency-focused lint rules.
    • Glossary: Quick definitions of concurrency terms.
  2. Capabilities of the Swift Concurrency Agent Skill

    main

    This skill provides expert guidance for AI coding tools to assist with:

    • Concurrency Decision Making: Choosing between async/await, actors, tasks, and task groups; understanding @MainActor, custom actors, and nonisolated usage; navigating isolation domains; and applying Sendable conformance.
    • Safe Code Writing: Avoiding actor reentrancy and retain cycles; preventing data races; handling task cancellation and error propagation; and managing memory in concurrent contexts.
    • Performance Optimization: Selecting between serialized, asynchronous, and parallel execution; reducing actor contention; and managing suspension points.
    • Swift 6 Migration: Step-by-step strategies for incremental strict concurrency checking, rewriting closure-based code, and migrating from Combine/RxSwift.
    • Testing: Writing reliable tests with Swift Testing or XCTest, handling @MainActor isolation, and using withMainSerialExecutor for determinism.
    • Core Data Integration: Safely passing data via NSManagedObjectID, implementing the DAO pattern, and avoiding Core Data concurrency pitfalls.
  3. What is a Task and how to use it

    main

    Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation. Use a Task when you need to start async work from synchronous code.

    Task entry isolation

    Task { ... } inherits the enclosing isolation domain. If your module uses defaultIsolation(MainActor.self), bare tasks will start on @MainActor by default.

    Decision Rule:

    • If the code before the first await needs to interact with the Main Actor (e.g., updating UI state), keep the inherited @MainActor entry.
    • If the code before the first await does not need the Main Actor, prefer Task { @concurrent in ... } to avoid unnecessary hops, and only hop back to the Main Actor for UI mutations.
    func synchronousMethod() {
        Task {
            await someAsyncMethod()
        }
    }
    
    // ✅ Prefix needs @MainActor; keep inherited main start
    Task {
        print("debug")        // trivial non-main line rides along
        self.isLoading = true  // main-actor state before first await
        await fetchData()
    }
    
    // ❌ Prefix has no main-actor work; first await hops away
    Task {
        await someActor.refresh()
    }
  4. Using isolated and nonisolated access

    main

    Actor methods are isolated by default. However, you can use specific patterns to manage isolation:

    • Isolated parameters: Use isolated in function parameters to inherit the caller's isolation, reducing the need for await inside the function.
    • Isolated closures: Pass an isolated actor instance into a closure to perform multiple operations within a single await call.
    • Nonisolated: Use the nonisolated keyword to opt out of isolation for immutable data or to satisfy protocol requirements (like CustomStringConvertible).
    // Isolated parameter
    struct Charger {
        static func charge(
            amount: Double,
            from account: isolated BankAccount
        ) async throws -> Double {
            try account.withdraw(amount: amount)
            return account.balance
        }
    }
    
    // Nonisolated for immutable data
    actor BankAccount {
        let accountHolder: String
        nonisolated var details: String { "Account: \(accountHolder)" }
    }
  5. Manage Tasks and Structured Concurrency

    main

    Structured Concurrency

    A pattern where child tasks have a well-defined relationship to parent tasks. Child tasks must complete before the parent scope exits. This provides automatic cancellation propagation and prevents orphaned tasks. It is implemented via async let and TaskGroup.

    Task Local

    Task-scoped storage that propagates through the task hierarchy. Declared with @TaskLocal, these values are accessible via the wrapper's static property. Child tasks automatically inherit their parent's task locals.

    Task priority

    A hint to the runtime regarding task importance. Priorities include:

    • .high
    • .medium
    • .low
    • .userInitiated
    • .utility
    • .background

    Note: Priority can escalate if a high-priority task awaits a low-priority one.

    Cancellation

    A cooperative mechanism. To implement cancellation, check Task.isCancelled or call Task.checkCancellation() (which throws) within long-running work. Cancellation propagates automatically to child tasks in structured concurrency.

  6. Use the Core Data decision tree for concurrency

    main

    When deciding how to interact with Core Data in a Swift Concurrency environment, follow this logic:

    • UI/View context? Use @MainActor + viewContext.
    • Background operation?
      • Quick operation? Use perform { } on a background context.
      • Batch operation? Use NSBatchDeleteRequest or NSBatchUpdateRequest.
    • Passing between contexts? Use NSManagedObjectID only.
    • Need a Sendable type?
      • Can refactor? Use the Data Access Object (DAO) pattern.
      • Can't refactor? Pass the NSManagedObjectID.
  7. Handling Actor Reentrancy

    main

    Critical: Actors are reentrant. When an actor method hits an await suspension point, the actor is unlocked, allowing other tasks to run. This means the actor's state might change between the start of the method and the resumption after the await.

    Rule: Never assume state is unchanged after an await. Complete all necessary state mutations before calling an asynchronous method if the subsequent logic depends on the state remaining consistent.

    actor BankAccount {
        var balance: Double
        
        func deposit(amount: Double) async {
            balance += amount
            print("Balance: \(balance)") // Do work before suspension
            
            // ⚠️ Actor unlocked during await
            await logActivity("Deposited \(amount)")
            
            // ⚠️ Balance may have changed here!
        }
    }
  8. Use the Data Access Objects (DAO) Pattern for Thread-Safe Data

    main

    The DAO pattern involves creating thread-safe, immutable value types (structs) that represent your Core Data entities. This allows you to safely pass data across isolation boundaries (like between an actor and the main thread) without passing the NSManagedObject itself.

    Implementation Pattern

    1. Define a Sendable struct (the DAO) that holds the properties of your entity.
    2. Implement an initializer on the DAO that accepts the NSManagedObject and extracts its values.

    Pros and Cons

    • Pros: Data is Sendable and safe to pass; immutable; provides a clear API for data transfer.
    • Cons: Requires boilerplate for each entity; requires rewriting fetch/mutation logic; adds an abstraction layer.
    // Managed object (not Sendable)
    @objc(Article)
    public class Article: NSManagedObject {
        @NSManaged public var title: String?
        @NSManaged public var timestamp: Date?
    }
    
    // DAO (Sendable)
    struct ArticleDAO: Sendable, Identifiable {
        let id: NSManagedObjectID
        let title: String
        let timestamp: Date
        
        init?(managedObject: Article) {
            guard let title = managedObject.title,
                  let timestamp = managedObject.timestamp else {
                return nil
            }
            self.id = managedObject.objectID
            self.title = title
            self.timestamp = timestamp
        }
    }
  9. When to use AsyncAlgorithms vs Standard Library vs SwiftUI

    main

    When migrating from Combine or RxSwift, select your tool based on the complexity of the asynchronous pattern required.

    Use Swift Async Algorithms for:

    • Time-based operations: debounce, throttle, timers.
    • Combining multiple async sequences: merge, combineLatest, zip.
    • Multi-consumer scenarios: AsyncChannel for backpressure.
    • Complex operator chains: FRP-like patterns.
    • Specific operators: removeDuplicates, chunks, adjacentPairs, compacted.

    Use the Standard Library for:

    • Bridging callbacks: AsyncStream is sufficient.
    • Simple iteration: for await in sequence.
    • Single-value operations: async/await.
    • Basic transformations: map, filter, contains.

    Use SwiftUI for:

    • UI observation: @Observable macro.
    • State management: @State, @Published properties.
    • User interactions: onChange, onReceive modifiers.
  10. Best practices for AsyncStream and AsyncSequence

    main

    To ensure reliable and leak-free asynchronous streams, follow these rules:

    1. Always call finish(): Streams stay alive indefinitely until you explicitly call continuation.finish(). Forgetting this causes memory leaks and hangs.
    2. Handle cancellation with onTermination: When bridging external APIs (like delegates or file descriptors), use continuation.onTermination to perform necessary cleanup.
    3. Use buffer policies wisely: Choose a bufferingPolicy (like .bufferingNewest(n)) that matches your use case. Be aware that if a consumer is slow, values may be dropped.
    4. Respect Task.isCancelled: In custom sequences or loops, check Task.isCancelled to stop execution promptly.
    5. Avoid sharing streams: Do not share a single AsyncStream across multiple consumers; values will be split unpredictably. Use AsyncChannel if you need a multi-consumer pattern.
    6. Use the throwing variant: If your source can fail, use AsyncThrowingStream instead of AsyncStream.
  11. Select the Right Concurrency Tool

    main

    Choose the appropriate concurrency primitive based on your specific requirement:

    • async/await: Default choice for sequential asynchronous work.
    • async let: Use for a fixed number of parallel operations known at compile time (auto-cancelled on throw).
    • withTaskGroup: Use for a dynamic number of parallel operations; provides structured concurrency (cancels children on scope exit).
    • Task { }: Use to bridge synchronous code to asynchronous code; inherits the current actor context.
    • Task.detached: Use only when you have a documented reason to escape the current actor context.
    • actor: Use for managing shared mutable state; preferred over locks or queues.
    • @MainActor: Use for state or code that is strictly UI-bound.