Claude Code Plugins

Community-maintained marketplace

Feedback

Swift 6 strict concurrency model, async/await, actors, @MainActor, Sendable, and thread safety. Use when user asks about concurrency, async/await, actors, MainActor, thread safety, data races, or migrating from GCD to structured concurrency.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

name swift-concurrency
description Swift 6 strict concurrency model, async/await, actors, @MainActor, Sendable, and thread safety. Use when user asks about concurrency, async/await, actors, MainActor, thread safety, data races, or migrating from GCD to structured concurrency.
allowed-tools Bash, Read, Write, Edit

Swift Concurrency

Comprehensive guide to Swift 6 strict concurrency, async/await patterns, actors, and modern thread-safe programming for iOS 26 and macOS Tahoe.

Prerequisites

  • Swift 6.x with strict concurrency enabled
  • Xcode 26+

Swift 6 Concurrency Model

Complete Concurrency Checking

Swift 6 enables complete data-race safety by default. The compiler statically verifies that your code is free from data races.

// In Package.swift
.target(
    name: "MyApp",
    swiftSettings: [
        .swiftLanguageMode(.v6)
    ]
)

Key Concepts

  1. Isolation Domains - Code is isolated to specific actors
  2. Sendable - Types that can safely cross isolation boundaries
  3. Actor Isolation - Data protected by actors
  4. MainActor - Main thread isolation for UI

Async/Await Basics

Async Functions

func fetchUser(id: Int) async throws -> User {
    let url = URL(string: "https://api.example.com/users/\(id)")!
    let (data, _) = try await URLSession.shared.data(from: url)
    return try JSONDecoder().decode(User.self, from: data)
}

// Calling async functions
func loadUserProfile() async {
    do {
        let user = try await fetchUser(id: 123)
        print("Loaded: \(user.name)")
    } catch {
        print("Failed: \(error)")
    }
}

Async Properties

struct ImageLoader {
    var url: URL

    var image: UIImage {
        get async throws {
            let (data, _) = try await URLSession.shared.data(from: url)
            guard let image = UIImage(data: data) else {
                throw ImageError.invalidData
            }
            return image
        }
    }
}

// Usage
let loader = ImageLoader(url: imageURL)
let image = try await loader.image

Async Sequences

func processLines(from url: URL) async throws {
    for try await line in url.lines {
        print(line)
    }
}

// Custom async sequence
struct Counter: AsyncSequence {
    typealias Element = Int
    let limit: Int

    struct AsyncIterator: AsyncIteratorProtocol {
        var current = 0
        let limit: Int

        mutating func next() async -> Int? {
            guard current < limit else { return nil }
            defer { current += 1 }
            try? await Task.sleep(for: .seconds(1))
            return current
        }
    }

    func makeAsyncIterator() -> AsyncIterator {
        AsyncIterator(limit: limit)
    }
}

// Usage
for await count in Counter(limit: 5) {
    print(count)  // Prints 0, 1, 2, 3, 4 with 1s delays
}

Task Management

Creating Tasks

// Unstructured task - runs independently
let task = Task {
    await performWork()
}

// Detached task - no inherited context
let detached = Task.detached {
    await performBackgroundWork()
}

// Task with priority
let highPriority = Task(priority: .high) {
    await performUrgentWork()
}

// Cancel a task
task.cancel()

// Check cancellation
func performWork() async throws {
    for item in items {
        try Task.checkCancellation()  // Throws if cancelled
        await process(item)
    }
}

Task Groups

func fetchAllUsers(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: User.self) { group in
        for id in ids {
            group.addTask {
                try await fetchUser(id: id)
            }
        }

        var users: [User] = []
        for try await user in group {
            users.append(user)
        }
        return users
    }
}

// With result ordering
func fetchUsersOrdered(ids: [Int]) async throws -> [User] {
    try await withThrowingTaskGroup(of: (Int, User).self) { group in
        for (index, id) in ids.enumerated() {
            group.addTask {
                (index, try await fetchUser(id: id))
            }
        }

        var results = [(Int, User)]()
        for try await result in group {
            results.append(result)
        }

        return results.sorted { $0.0 < $1.0 }.map(\.1)
    }
}

Discarding Task Group (Swift 6)

// For fire-and-forget parallel tasks
await withDiscardingTaskGroup { group in
    for url in urls {
        group.addTask {
            await prefetchImage(from: url)
        }
    }
    // Results are automatically discarded
}

Actors

Basic Actor

actor BankAccount {
    private var balance: Decimal = 0

    func deposit(_ amount: Decimal) {
        balance += amount
    }

    func withdraw(_ amount: Decimal) throws {
        guard balance >= amount else {
            throw BankError.insufficientFunds
        }
        balance -= amount
    }

    func getBalance() -> Decimal {
        balance
    }
}

// Usage - all calls are async
let account = BankAccount()
await account.deposit(100)
let balance = await account.getBalance()

Nonisolated Members

actor DataManager {
    private var cache: [String: Data] = [:]

    // Isolated - requires await
    func store(_ data: Data, for key: String) {
        cache[key] = data
    }

    // Nonisolated - can be called synchronously
    nonisolated let identifier = UUID()

    nonisolated func createKey(for name: String) -> String {
        "\(identifier)-\(name)"
    }
}

let manager = DataManager()
let key = manager.createKey(for: "test")  // No await needed
await manager.store(data, for: key)        // Requires await

Actor Reentrancy

actor ImageCache {
    private var cache: [URL: UIImage] = [:]
    private var inProgress: [URL: Task<UIImage, Error>] = [:]

    func image(for url: URL) async throws -> UIImage {
        // Check cache first
        if let cached = cache[url] {
            return cached
        }

        // Check if already loading
        if let existing = inProgress[url] {
            return try await existing.value
        }

        // Start new load
        let task = Task {
            let (data, _) = try await URLSession.shared.data(from: url)
            let image = UIImage(data: data)!
            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
        }
    }
}

@MainActor

Class-Level Isolation

@MainActor
class ViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var isLoading = false
    @Published var error: Error?

    func loadItems() async {
        isLoading = true
        defer { isLoading = false }

        do {
            items = try await fetchItems()
        } catch {
            self.error = error
        }
    }

    // Nonisolated for background work
    nonisolated func fetchItems() async throws -> [Item] {
        let url = URL(string: "https://api.example.com/items")!
        let (data, _) = try await URLSession.shared.data(from: url)
        return try JSONDecoder().decode([Item].self, from: data)
    }
}

Method-Level Isolation

class DataProcessor {
    func processData() async -> ProcessedData {
        // Runs on background
        let result = await heavyComputation()
        return result
    }

    @MainActor
    func updateUI(with data: ProcessedData) {
        // Runs on main thread
        label.text = data.summary
    }
}

MainActor.run

func performBackgroundWork() async {
    let result = await processLargeDataset()

    // Jump to main actor for UI update
    await MainActor.run {
        updateProgressView(with: result)
    }
}

Sendable Protocol

Sendable Types

// Value types are implicitly Sendable
struct Point: Sendable {
    var x: Double
    var y: Double
}

// Immutable classes can be Sendable
final class Configuration: Sendable {
    let apiKey: String
    let baseURL: URL

    init(apiKey: String, baseURL: URL) {
        self.apiKey = apiKey
        self.baseURL = baseURL
    }
}

// Actors are implicitly Sendable
actor Counter: Sendable {
    var count = 0
}

@unchecked Sendable

// Use carefully for types with internal synchronization
final class ThreadSafeCache: @unchecked Sendable {
    private let lock = NSLock()
    private var storage: [String: Any] = [:]

    func get(_ key: String) -> Any? {
        lock.lock()
        defer { lock.unlock() }
        return storage[key]
    }

    func set(_ key: String, value: Any) {
        lock.lock()
        defer { lock.unlock() }
        storage[key] = value
    }
}

Sendable Closures

// @Sendable closures can be sent across isolation boundaries
func performAsync(_ work: @Sendable @escaping () async -> Void) {
    Task {
        await work()
    }
}

// Capturing in Sendable closures
func processItems(_ items: [Item]) {
    // items must be Sendable
    Task { @Sendable in
        for item in items {
            await process(item)
        }
    }
}

Swift 6.2 Improvements

Default Isolation

// New in Swift 6.2: Default to main actor isolation
// In Package.swift or build settings:
// -default-isolation MainActor

// Or per-file:
@MainActor
extension MyView {
    // All methods here are MainActor-isolated by default
}

Observations Async Sequence

import Observation

@Observable
class Model {
    var count = 0
}

// Stream changes with async sequence
func observeModel(_ model: Model) async {
    for await _ in model.observations(of: \.count) {
        print("Count changed to: \(model.count)")
    }
}

Region-Based Isolation (SE-0414)

// Compiler can now reason about value regions
func processData() async {
    var data = [1, 2, 3]

    // Compiler knows data doesn't escape
    await withTaskGroup(of: Void.self) { group in
        for item in data {
            group.addTask {
                print(item)
            }
        }
    }

    // Safe to mutate after task group completes
    data.append(4)
}

Common Pitfalls

Pitfall 1: DispatchQueue in Swift 6

// WRONG - flagged as unsafe in Swift 6
DispatchQueue.main.async {
    self.updateUI()
}

// CORRECT - use MainActor
await MainActor.run {
    self.updateUI()
}

// Or make the enclosing context @MainActor
@MainActor
func handleResult() {
    updateUI()  // Already on main actor
}

Pitfall 2: Combine without Isolation

// WRONG - closure isolation unclear
publisher
    .sink { value in
        self.items = value  // Data race potential
    }
    .store(in: &cancellables)

// CORRECT - explicit isolation
publisher
    .receive(on: DispatchQueue.main)
    .sink { [weak self] value in
        Task { @MainActor in
            self?.items = value
        }
    }
    .store(in: &cancellables)

Pitfall 3: Non-Sendable Captures

// WRONG - UIViewController not Sendable
func saveData() {
    let vc = self  // UIViewController
    Task {
        await save()
        vc.showSuccess()  // Error: captured non-Sendable
    }
}

// CORRECT - use weak capture and MainActor
func saveData() {
    Task { [weak self] in
        await save()
        await MainActor.run {
            self?.showSuccess()
        }
    }
}

Pitfall 4: Actor Reentrancy Surprises

actor DataStore {
    var items: [Item] = []

    func addItem(_ item: Item) async {
        // State before await
        let countBefore = items.count

        await saveToDatabase(item)

        // WARNING: items may have changed during await!
        items.append(item)  // Could cause duplicates
    }

    // BETTER - capture state carefully
    func addItemSafely(_ item: Item) async {
        items.append(item)
        let itemsCopy = items
        await saveToDatabase(itemsCopy)
    }
}

Migration from GCD

Before (GCD)

func loadData(completion: @escaping (Result<Data, Error>) -> Void) {
    DispatchQueue.global().async {
        do {
            let data = try self.fetchDataSync()
            DispatchQueue.main.async {
                completion(.success(data))
            }
        } catch {
            DispatchQueue.main.async {
                completion(.failure(error))
            }
        }
    }
}

After (Async/Await)

func loadData() async throws -> Data {
    try await fetchData()
}

// Usage
Task {
    do {
        let data = try await loadData()
        // Already on calling context
    } catch {
        // Handle error
    }
}

Bridging Completion Handlers

// Wrap completion handler API
func fetchLegacyData() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        legacyFetchData { result in
            switch result {
            case .success(let data):
                continuation.resume(returning: data)
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

// Important: Only call resume ONCE
func fetchWithTimeout() async throws -> Data {
    try await withCheckedThrowingContinuation { continuation in
        var hasResumed = false

        legacyFetch { data in
            guard !hasResumed else { return }
            hasResumed = true
            continuation.resume(returning: data)
        }

        DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
            guard !hasResumed else { return }
            hasResumed = true
            continuation.resume(throwing: TimeoutError())
        }
    }
}

AsyncStream

Creating Streams

func notifications() -> AsyncStream<Notification> {
    AsyncStream { continuation in
        let observer = NotificationCenter.default.addObserver(
            forName: .customNotification,
            object: nil,
            queue: .main
        ) { notification in
            continuation.yield(notification)
        }

        continuation.onTermination = { _ in
            NotificationCenter.default.removeObserver(observer)
        }
    }
}

// Usage
for await notification in notifications() {
    print("Received: \(notification)")
}

Throwing Streams

func dataStream(from url: URL) -> AsyncThrowingStream<Data, Error> {
    AsyncThrowingStream { continuation in
        let task = URLSession.shared.dataTask(with: url) { data, _, error in
            if let error = error {
                continuation.finish(throwing: error)
                return
            }
            if let data = data {
                continuation.yield(data)
            }
            continuation.finish()
        }
        task.resume()

        continuation.onTermination = { _ in
            task.cancel()
        }
    }
}

Testing Concurrent Code

import Testing

@Test("Concurrent counter increments correctly")
func concurrentCounter() async {
    let counter = Counter()

    await withTaskGroup(of: Void.self) { group in
        for _ in 0..<1000 {
            group.addTask {
                await counter.increment()
            }
        }
    }

    let final = await counter.value
    #expect(final == 1000)
}

@Test(.serialized, "Tests that must run sequentially")
func sequentialTest() async {
    // Use .serialized trait for tests not ready for parallel
}

Best Practices

  1. Prefer structured concurrency - Use task groups over detached tasks
  2. Make types Sendable - Design for thread safety from the start
  3. Use actors for shared mutable state - Don't use locks in Swift 6
  4. Isolate UI code to MainActor - Use @MainActor for ViewModels
  5. Handle cancellation - Check Task.isCancelled in long operations
  6. Avoid async in tight loops - Batch work when possible
  7. Test concurrent code - Use .serialized trait when needed

Official Resources