diff --git a/.claude/skills/swift-concurrency/SKILL.md b/.claude/skills/swift-concurrency/SKILL.md new file mode 100644 index 0000000..524b074 --- /dev/null +++ b/.claude/skills/swift-concurrency/SKILL.md @@ -0,0 +1,235 @@ +--- +name: swift-concurrency +description: 'Expert guidance on Swift Concurrency best practices, patterns, and implementation. Use when developers mention: (1) Swift Concurrency, async/await, actors, or tasks, (2) "use Swift Concurrency" or "modern concurrency patterns", (3) migrating to Swift 6, (4) data races or thread safety issues, (5) refactoring closures to async/await, (6) @MainActor, Sendable, or actor isolation, (7) concurrent code architecture or performance optimization, (8) concurrency-related linter warnings (SwiftLint or similar; e.g. async_without_await, Sendable/actor isolation/MainActor lint).' +--- +# Swift Concurrency + +## Overview + +This skill provides expert guidance on Swift Concurrency, covering modern async/await patterns, actors, tasks, Sendable conformance, and migration to Swift 6. Use this skill to help developers write safe, performant concurrent code and navigate the complexities of Swift's structured concurrency model. + +## Agent Behavior Contract (Follow These Rules) + +1. Analyze the project/package file to find out which Swift language mode (Swift 5.x vs Swift 6) and which Xcode/Swift toolchain is used when advice depends on it. +2. Before proposing fixes, identify the isolation boundary: `@MainActor`, custom actor, actor instance isolation, or nonisolated. +3. Do not recommend `@MainActor` as a blanket fix. Justify why main-actor isolation is correct for the code. +4. Prefer structured concurrency (child tasks, task groups) over unstructured tasks. Use `Task.detached` only with a clear reason. +5. If recommending `@preconcurrency`, `@unchecked Sendable`, or `nonisolated(unsafe)`, require: + - a documented safety invariant + - a follow-up ticket to remove or migrate it +6. For migration work, optimize for minimal blast radius (small, reviewable changes) and add verification steps. +7. Course references are for deeper learning only. Use them sparingly and only when they clearly help answer the developer’s question. + +## Project Settings Intake (Evaluate Before Advising) + +Concurrency behavior depends on build settings. Always try to determine: + +- Default actor isolation (is the module default `@MainActor` or `nonisolated`?) +- Strict concurrency checking level (minimal/targeted/complete) +- Whether upcoming features are enabled (especially `NonisolatedNonsendingByDefault`) +- Swift language mode (Swift 5.x vs Swift 6) and SwiftPM tools version + +### Manual checks (no scripts) + +- SwiftPM: + - Check `Package.swift` for `.defaultIsolation(MainActor.self)`. + - Check `Package.swift` for `.enableUpcomingFeature("NonisolatedNonsendingByDefault")`. + - Check for strict concurrency flags: `.enableExperimentalFeature("StrictConcurrency=targeted")` (or similar). + - Check tools version at the top: `// swift-tools-version: ...` +- Xcode projects: + - Search `project.pbxproj` for: + - `SWIFT_DEFAULT_ACTOR_ISOLATION` + - `SWIFT_STRICT_CONCURRENCY` + - `SWIFT_UPCOMING_FEATURE_` (and/or `SWIFT_ENABLE_EXPERIMENTAL_FEATURES`) + +If any of these are unknown, ask the developer to confirm them before giving migration-sensitive guidance. + +## Quick Decision Tree + +When a developer needs concurrency guidance, follow this decision tree: + +1. **Starting fresh with async code?** + - Read `references/async-await-basics.md` for foundational patterns + - For parallel operations → `references/tasks.md` (async let, task groups) + +2. **Protecting shared mutable state?** + - Need to protect class-based state → `references/actors.md` (actors, @MainActor) + - Need thread-safe value passing → `references/sendable.md` (Sendable conformance) + +3. **Managing async operations?** + - Structured async work → `references/tasks.md` (Task, child tasks, cancellation) + - Streaming data → `references/async-sequences.md` (AsyncSequence, AsyncStream) + +4. **Working with legacy frameworks?** + - Core Data integration → `references/core-data.md` + - General migration → `references/migration.md` + +5. **Performance or debugging issues?** + - Slow async code → `references/performance.md` (profiling, suspension points) + - Testing concerns → `references/testing.md` (XCTest, Swift Testing) + +6. **Understanding threading behavior?** + - Read `references/threading.md` for thread/task relationship and isolation + +7. **Memory issues with tasks?** + - Read `references/memory-management.md` for retain cycle prevention + +## Triage-First Playbook (Common Errors -> Next Best Move) + +- SwiftLint concurrency-related warnings + - Use `references/linting.md` for rule intent and preferred fixes; avoid dummy awaits as “fixes”. +- SwiftLint `async_without_await` warning + - Remove `async` if not required; if required by protocol/override/@concurrent, prefer narrow suppression over adding fake awaits. See `references/linting.md`. +- "Sending value of non-Sendable type ... risks causing data races" + - First: identify where the value crosses an isolation boundary + - Then: use `references/sendable.md` and `references/threading.md` (especially Swift 6.2 behavior changes) +- "Main actor-isolated ... cannot be used from a nonisolated context" + - First: decide if it truly belongs on `@MainActor` + - Then: use `references/actors.md` (global actors, `nonisolated`, isolated parameters) and `references/threading.md` (default isolation) +- "Class property 'current' is unavailable from asynchronous contexts" (Thread APIs) + - Use `references/threading.md` to avoid thread-centric debugging and rely on isolation + Instruments +- XCTest async errors like "wait(...) is unavailable from asynchronous contexts" + - Use `references/testing.md` (`await fulfillment(of:)` and Swift Testing patterns) +- Core Data concurrency warnings/errors + - Use `references/core-data.md` (DAO/`NSManagedObjectID`, default isolation conflicts) + +## Core Patterns Reference + +### When to Use Each Concurrency Tool + +**async/await** - Making existing synchronous code asynchronous +```swift +// Use for: Single asynchronous operations +func fetchUser() async throws -> User { + try await networkClient.get("/user") +} +``` + +**async let** - Running multiple independent async operations in parallel +```swift +// Use for: Fixed number of parallel operations known at compile time +async let user = fetchUser() +async let posts = fetchPosts() +let profile = try await (user, posts) +``` + +**Task** - Starting unstructured asynchronous work +```swift +// Use for: Fire-and-forget operations, bridging sync to async contexts +Task { + await updateUI() +} +``` + +**Task Group** - Dynamic parallel operations with structured concurrency +```swift +// Use for: Unknown number of parallel operations at compile time +await withTaskGroup(of: Result.self) { group in + for item in items { + group.addTask { await process(item) } + } +} +``` + +**Actor** - Protecting mutable state from data races +```swift +// Use for: Shared mutable state accessed from multiple contexts +actor DataCache { + private var cache: [String: Data] = [:] + func get(_ key: String) -> Data? { cache[key] } +} +``` + +**@MainActor** - Ensuring UI updates on main thread +```swift +// Use for: View models, UI-related classes +@MainActor +class ViewModel: ObservableObject { + @Published var data: String = "" +} +``` + +### Common Scenarios + +**Scenario: Network request with UI update** +```swift +Task { @concurrent in + let data = try await fetchData() // Background + await MainActor.run { + self.updateUI(with: data) // Main thread + } +} +``` + +**Scenario: Multiple parallel network requests** +```swift +async let users = fetchUsers() +async let posts = fetchPosts() +async let comments = fetchComments() +let (u, p, c) = try await (users, posts, comments) +``` + +**Scenario: Processing array items in parallel** +```swift +await withTaskGroup(of: ProcessedItem.self) { group in + for item in items { + group.addTask { await process(item) } + } + for await result in group { + results.append(result) + } +} +``` + +## Swift 6 Migration Quick Guide + +Key changes in Swift 6: +- **Strict concurrency checking** enabled by default +- **Complete data-race safety** at compile time +- **Sendable requirements** enforced on boundaries +- **Isolation checking** for all async boundaries + +For detailed migration steps, see `references/migration.md`. + +## Reference Files + +Load these files as needed for specific topics: + +- **`async-await-basics.md`** - async/await syntax, execution order, async let, URLSession patterns +- **`tasks.md`** - Task lifecycle, cancellation, priorities, task groups, structured vs unstructured +- **`threading.md`** - Thread/task relationship, suspension points, isolation domains, nonisolated +- **`memory-management.md`** - Retain cycles in tasks, memory safety patterns +- **`actors.md`** - Actor isolation, @MainActor, global actors, reentrancy, custom executors, Mutex +- **`sendable.md`** - Sendable conformance, value/reference types, @unchecked, region isolation +- **`linting.md`** - Concurrency-focused lint rules and SwiftLint `async_without_await` +- **`async-sequences.md`** - AsyncSequence, AsyncStream, when to use vs regular async methods +- **`core-data.md`** - NSManagedObject sendability, custom executors, isolation conflicts +- **`performance.md`** - Profiling with Instruments, reducing suspension points, execution strategies +- **`testing.md`** - XCTest async patterns, Swift Testing, concurrency testing utilities +- **`migration.md`** - Swift 6 migration strategy, closure-to-async conversion, @preconcurrency, FRP migration + +## Best Practices Summary + +1. **Prefer structured concurrency** - Use task groups over unstructured tasks when possible +2. **Minimize suspension points** - Keep actor-isolated sections small to reduce context switches +3. **Use @MainActor judiciously** - Only for truly UI-related code +4. **Make types Sendable** - Enable safe concurrent access by conforming to Sendable +5. **Handle cancellation** - Check Task.isCancelled in long-running operations +6. **Avoid blocking** - Never use semaphores or locks in async contexts +7. **Test concurrent code** - Use proper async test methods and consider timing issues + +## Verification Checklist (When You Change Concurrency Code) + +- Confirm build settings (default isolation, strict concurrency, upcoming features) before interpreting diagnostics. +- After refactors: + - Run tests, especially concurrency-sensitive ones (see `references/testing.md`). + - If performance-related, verify with Instruments (see `references/performance.md`). + - If lifetime-related, verify deinit/cancellation behavior (see `references/memory-management.md`). + +## Glossary + +See `references/glossary.md` for quick definitions of core concurrency terms used across this skill. + +--- + +**Note**: This skill is based on the comprehensive [Swift Concurrency Course](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=skill-footer) by Antoine van der Lee. diff --git a/.claude/skills/swift-concurrency/references/actors.md b/.claude/skills/swift-concurrency/references/actors.md new file mode 100644 index 0000000..651d990 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/actors.md @@ -0,0 +1,640 @@ +# Actors + +Data isolation patterns and thread-safe state management in Swift. + +## What is an Actor? + +Actors protect mutable state by ensuring only one task accesses it at a time. They're reference types with automatic synchronization. + +```swift +actor Counter { + var value = 0 + + func increment() { + value += 1 + } +} +``` + +**Key guarantee**: Only one task can access mutable state at a time (serialized access). + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.1: Understanding actors in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Actor Isolation + +### Enforced by compiler + +```swift +actor BankAccount { + var balance: Int = 0 + + func deposit(_ amount: Int) { + balance += amount + } +} + +let account = BankAccount() +account.balance += 1 // ❌ Error: can't mutate from outside +await account.deposit(1) // ✅ Must use actor's methods +``` + +### Reading properties + +```swift +let account = BankAccount() +await account.deposit(100) +print(await account.balance) // Must await reads too +``` + +Always use `await` when accessing actor properties/methods—you don't know if another task is inside. + +## Actors vs Classes + +### Similarities + +- Reference types (copies share same instance) +- Can have properties, methods, initializers +- Can conform to protocols + +### Differences + +- **No inheritance** (except `NSObject` for Objective-C interop) +- **Automatic isolation** (no manual locks needed) +- **Implicit Sendable** conformance + +```swift +// ❌ Can't inherit from actors +actor Base {} +actor Child: Base {} // Error + +// ✅ NSObject exception +actor Example: NSObject {} // OK for Objective-C +``` + +## Global Actors + +Shared isolation domain across types, functions, and properties. + +### @MainActor + +Ensures execution on main thread: + +```swift +@MainActor +final class ViewModel { + var items: [Item] = [] +} + +@MainActor +func updateUI() { + // Always runs on main thread +} + +@MainActor +var title: String = "" +``` + +### Custom global actors + +```swift +@globalActor +actor ImageProcessing { + static let shared = ImageProcessing() + private init() {} // Prevent duplicate instances +} + +@ImageProcessing +final class ImageCache { + var images: [URL: Data] = [:] +} + +@ImageProcessing +func applyFilter(_ image: UIImage) -> UIImage { + // All image processing serialized +} +``` + +**Use private init** to prevent creating multiple executors. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.2: An introduction to Global Actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## @MainActor Best Practices + +### When to use + +UI-related code that must run on main thread: + +```swift +@MainActor +final class ContentViewModel: ObservableObject { + @Published var items: [Item] = [] +} +``` + +### Replacing DispatchQueue.main + +```swift +// Old way +DispatchQueue.main.async { + // Update UI +} + +// Modern way +await MainActor.run { + // Update UI +} + +// Better: Use attribute +@MainActor +func updateUI() { + // Automatically on main thread +} +``` + +### MainActor.assumeIsolated + +**Use sparingly** - assumes you're on main thread, crashes if not: + +```swift +func methodB() { + assert(Thread.isMainThread) // Validate assumption + + MainActor.assumeIsolated { + someMainActorMethod() + } +} +``` + +**Prefer**: Explicit `@MainActor` or `await MainActor.run` over `assumeIsolated`. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.3: When and how to use @MainActor](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Isolated vs Nonisolated + +### Default: Isolated + +Actor methods are isolated by default: + +```swift +actor BankAccount { + var balance: Double + + // Implicitly isolated + func deposit(_ amount: Double) { + balance += amount + } +} +``` + +### Isolated parameters + +Reduce suspension points by inheriting caller's isolation: + +```swift +struct Charger { + static func charge( + amount: Double, + from account: isolated BankAccount + ) async throws -> Double { + // No await needed - we're isolated to account + try account.withdraw(amount: amount) + return account.balance + } +} +``` + +### Isolated closures + +```swift +actor Database { + func transaction( + _ operation: @Sendable (_ db: isolated Database) throws -> T + ) throws -> T { + beginTransaction() + let result = try operation(self) + commitTransaction() + return result + } +} + +// Usage: Multiple operations, one await +try await database.transaction { db in + db.insert(item1) + db.insert(item2) + db.insert(item3) +} +``` + +### Generic isolated extension + +```swift +extension Actor { + func performInIsolation( + _ block: @Sendable (_ actor: isolated Self) throws -> T + ) async rethrows -> T { + try block(self) + } +} + +// Usage +try await bankAccount.performInIsolation { account in + try account.withdraw(amount: 20) + print("Balance: \(account.balance)") +} +``` + +### Nonisolated + +Opt out of isolation for immutable data: + +```swift +actor BankAccount { + let accountHolder: String + + nonisolated var details: String { + "Account: \(accountHolder)" + } +} + +// No await needed +print(account.details) +``` + +### Protocol conformance + +```swift +extension BankAccount: CustomStringConvertible { + nonisolated var description: String { + "Account: \(accountHolder)" + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.4: Isolated vs. non-isolated access in actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Isolated Deinit (Swift 6.2+) + +Clean up actor state on deallocation: + +```swift +actor FileDownloader { + var downloadTask: Task? + + isolated deinit { + downloadTask?.cancel() // Can call isolated methods + } +} +``` + +**Requires**: iOS 18.4+, macOS 15.4+ + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.5: Using Isolated synchronous deinit](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Global Actor Isolated Conformance (Swift 6.2+) + +Protocol conformance respecting actor isolation: + +```swift +@MainActor +final class PersonViewModel { + let id: UUID + var name: String +} + +extension PersonViewModel: @MainActor Equatable { + static func == (lhs: PersonViewModel, rhs: PersonViewModel) -> Bool { + lhs.id == rhs.id && lhs.name == rhs.name + } +} +``` + +**Enable**: `InferIsolatedConformances` upcoming feature. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.6: Adding isolated conformance to protocols](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Actor Reentrancy + +**Critical**: State can change between suspension points. + +```swift +actor BankAccount { + var balance: Double + + func deposit(amount: Double) async { + balance += amount + + // ⚠️ Actor unlocked during await + await logActivity("Deposited \(amount)") + + // ⚠️ Balance may have changed! + print("Balance: \(balance)") + } +} +``` + +### Problem + +```swift +async let _ = account.deposit(50) +async let _ = account.deposit(50) +async let _ = account.deposit(50) + +// May print same balance three times: +// Balance: 150 +// Balance: 150 +// Balance: 150 +``` + +### Solution + +Complete actor work before suspending: + +```swift +func deposit(amount: Double) async { + balance += amount + print("Balance: \(balance)") // Before suspension + + await logActivity("Deposited \(amount)") +} +``` + +**Rule**: Don't assume state is unchanged after `await`. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.7: Understanding actor reentrancy](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## #isolation Macro + +Inherit caller's isolation for generic code: + +```swift +extension Collection where Element: Sendable { + func sequentialMap( + isolation: isolated (any Actor)? = #isolation, + transform: (Element) async -> Result + ) async -> [Result] { + var results: [Result] = [] + for element in self { + results.append(await transform(element)) + } + return results + } +} + +// Usage from @MainActor context +Task { @MainActor in + let names = ["Alice", "Bob"] + let results = await names.sequentialMap { name in + await process(name) // Inherits @MainActor + } +} +``` + +**Benefits**: Avoids unnecessary suspensions, allows non-Sendable data. + +### Task Closures and Isolation Inheritance + +When spawning unstructured `Task` closures that need to work with `non-Sendable` types, you must capture the isolation parameter to inherit the caller's isolation context. + +**Problem**: `Task` closures are `@Sendable`, which prevents capturing `non-Sendable` types: + +```swift +func process(delegate: NonSendableDelegate) { + Task { + delegate.doWork() // ❌ Error: capturing non-Sendable type + } +} +``` + +**Solution**: Use `#isolation` parameter and capture it inside the `Task`: + +```swift +func process( + delegate: NonSendableDelegate, + isolation: isolated (any Actor)? = #isolation +) { + Task { + _ = isolation // Forces capture, Task inherits caller's isolation + delegate.doWork() // ✅ Safe - running on caller's actor + } +} +``` + +**Why `_ = isolation` is required**: Per [SE-0420](https://github.com/swiftlang/swift-evolution/blob/main/proposals/0420-inheritance-of-actor-isolation.md), `Task` closures only inherit isolation when "a non-optional binding of an isolated parameter is captured by the closure." The `_ = isolation` statement forces this capture. The capture list syntax `[isolation]` should work but currently does not. + +**When to use this pattern**: +- Spawning `Task`s that work with `non-Sendable` delegate objects +- Fire-and-forget async work that needs access to caller's state +- Bridging callback-based APIs to async streams while keeping delegates alive + +**Note**: This pattern keeps the `non-Sendable` value alive and accessible within the `Task`. The `Task` runs on the caller's isolation domain, so no cross-isolation "sending" occurs. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.8: Inheritance of actor isolation using the #isolation macro](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Custom Actor Executors + +**Advanced**: Control how actor schedules work. + +### Serial executor + +```swift +final class DispatchQueueExecutor: SerialExecutor { + private let queue: DispatchQueue + + init(queue: DispatchQueue) { + self.queue = queue + } + + func enqueue(_ job: consuming ExecutorJob) { + let unownedJob = UnownedJob(job) + let executor = asUnownedSerialExecutor() + + queue.async { + unownedJob.runSynchronously(on: executor) + } + } +} + +actor LoggingActor { + private let executor: DispatchQueueExecutor + + nonisolated var unownedExecutor: UnownedSerialExecutor { + executor.asUnownedSerialExecutor() + } + + init(queue: DispatchQueue) { + executor = DispatchQueueExecutor(queue: queue) + } +} +``` + +### When to use + +- Integration with legacy DispatchQueue-based code +- Specific thread requirements (e.g., C++ interop) +- Custom scheduling logic + +**Default executor is usually sufficient.** + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.9: Using a custom actor executor](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Mutex: Alternative to Actors + +Synchronous locking without async/await overhead (iOS 18+, macOS 15+). + +### Basic usage + +```swift +import Synchronization + +final class Counter { + private let count = Mutex(0) + + var currentCount: Int { + count.withLock { $0 } + } + + func increment() { + count.withLock { $0 += 1 } + } +} +``` + +### Sendable access to non-Sendable types + +```swift +final class TouchesCapturer: Sendable { + let path = Mutex(NSBezierPath()) + + func storeTouch(_ point: NSPoint) { + path.withLock { path in + path.move(to: point) + } + } +} +``` + +### Error handling + +```swift +func decrement() throws { + try count.withLock { count in + guard count > 0 else { + throw Error.reachedZero + } + count -= 1 + } +} +``` + +### Mutex vs Actor + +| Feature | Mutex | Actor | +|---------|-------|-------| +| Synchronous | ✅ | ❌ (requires await) | +| Async support | ❌ | ✅ | +| Thread blocking | ✅ | ❌ (cooperative) | +| Fine-grained locking | ✅ | ❌ (whole actor) | +| Legacy code integration | ✅ | ❌ | + +**Use Mutex when**: +- Need synchronous access +- Working with legacy non-async APIs +- Fine-grained locking required +- Low contention, short critical sections + +**Use Actor when**: +- Can adopt async/await +- Need logical isolation +- Working in async context + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 5.10: Using a Mutex as an alternative to actors](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Common Patterns + +### View model with @MainActor + +```swift +@MainActor +final class ContentViewModel: ObservableObject { + @Published var items: [Item] = [] + + func loadItems() async { + items = try await api.fetchItems() + } +} +``` + +### Background processing with custom actor + +```swift +@ImageProcessing +final class ImageProcessor { + func process(_ images: [UIImage]) async -> [UIImage] { + images.map { applyFilters($0) } + } +} +``` + +### Mixed isolation + +```swift +actor DataStore { + private var items: [Item] = [] + + func add(_ item: Item) { + items.append(item) + } + + nonisolated func itemCount() -> Int { + // ❌ Can't access items + return 0 + } +} +``` + +### Transaction pattern + +```swift +actor Database { + func transaction( + _ operation: @Sendable (_ db: isolated Database) throws -> T + ) throws -> T { + beginTransaction() + defer { commitTransaction() } + return try operation(self) + } +} +``` + +## Best Practices + +1. **Prefer actors over manual locks** for async code +2. **Use @MainActor for UI** - all view models, UI updates +3. **Minimize work in actors** - keep critical sections short +4. **Watch for reentrancy** - don't assume state unchanged after await +5. **Use nonisolated sparingly** - only for truly immutable data +6. **Avoid assumeIsolated** - prefer explicit isolation +7. **Custom executors are rare** - default is usually best +8. **Consider Mutex for sync code** - when async overhead not needed +9. **Complete actor work before suspending** - prevent reentrancy bugs +10. **Use isolated parameters** - reduce suspension points + +## Decision Tree + +``` +Need thread-safe mutable state? +├─ Async context? +│ ├─ Single instance? → Actor +│ ├─ Global/shared? → Global Actor (@MainActor, custom) +│ └─ UI-related? → @MainActor +│ +└─ Synchronous context? + ├─ Can refactor to async? → Actor + ├─ Legacy code integration? → Mutex + └─ Fine-grained locking? → Mutex +``` + +## Further Learning + +For migration strategies, advanced patterns, and real-world examples, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/async-await-basics.md b/.claude/skills/swift-concurrency/references/async-await-basics.md new file mode 100644 index 0000000..3b1dcdc --- /dev/null +++ b/.claude/skills/swift-concurrency/references/async-await-basics.md @@ -0,0 +1,249 @@ +# Async/Await Basics + +Core patterns and best practices for async/await in Swift. + +## Function Declaration + +Mark functions with `async` to indicate asynchronous work: + +```swift +func fetchData() async -> Data { + // async work +} + +func fetchData() async throws -> Data { + // async work that can fail +} +``` + +**Key benefit over closures**: The compiler enforces return values. No forgotten completion handlers. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 2.1: Introduction to async/await syntax](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Calling Async Functions + +### From synchronous context + +Use `Task` to bridge from sync to async: + +```swift +Task { + let data = try await fetchData() +} +``` + +### From async context + +Use `await` directly: + +```swift +func processData() async throws { + let data = try await fetchData() + // process data +} +``` + +## Execution Order + +Structured concurrency executes top-to-bottom in the order you expect: + +```swift +let first = try await fetchData(1) // Waits for completion +let second = try await fetchData(2) // Starts after first completes +let third = try await fetchData(3) // Starts after second completes +``` + +Code after `await` only executes once the awaited function returns. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 2.2: Understanding the order of execution](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Parallel Execution with async let + +Use `async let` to run multiple operations concurrently: + +```swift +async let data1 = fetchData(1) +async let data2 = fetchData(2) +async let data3 = fetchData(3) + +let results = try await [data1, data2, data3] +``` + +### How async let works + +- **Starts immediately**: The function executes right away, even before `await` +- **Structured concurrency**: Automatically canceled when leaving scope +- **Error handling**: If one fails, others are implicitly canceled when awaiting grouped results +- **No redundant keywords**: Don't use `try await` in the `async let` line itself + +```swift +// Redundant - avoid this +async let data = try await fetchData() + +// Correct - errors handled at await point +async let data = fetchData() +let result = try await data +``` + +### When to use async let + +**Use when:** +- Tasks don't depend on each other +- Number of tasks known at compile-time +- Want automatic cancellation on scope exit + +**Avoid when:** +- Tasks must run sequentially +- Need dynamic task spawning (use `TaskGroup`) +- Need manual cancellation control + +### Limitations + +- Cannot use at top-level declarations (only within function bodies) +- Tasks not explicitly awaited may be canceled implicitly + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 2.3: Calling async functions in parallel using async let](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## URLSession with Async/Await + +URLSession provides async alternatives to closure-based APIs: + +```swift +// Closure-based (old) +URLSession.shared.dataTask(with: request) { data, response, error in + guard let data = data, error == nil else { return } + // handle response +}.resume() + +// Async/await (modern) +let (data, response) = try await URLSession.shared.data(for: request) +``` + +### Benefits over closures + +- No optional `data` or `response` to unwrap +- Automatic error throwing +- Compiler enforces return values +- Simpler error handling with do-catch + +### Complete network request pattern + +```swift +func fetchUser(id: Int) async throws -> User { + let url = URL(string: "https://api.example.com/users/\(id)")! + var request = URLRequest(url: url) + request.httpMethod = "GET" + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + + return try JSONDecoder().decode(User.self, from: data) +} +``` + +### POST requests with JSON + +```swift +func createUser(_ user: User) async throws -> User { + let url = URL(string: "https://api.example.com/users")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.httpBody = try JSONEncoder().encode(user) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, + (200...299).contains(httpResponse.statusCode) else { + throw NetworkError.invalidResponse + } + + return try JSONDecoder().decode(User.self, from: data) +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 2.4: Performing network requests using URLSession and async/await](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Typed Errors (Swift 6) + +Specify exact error types for better API contracts: + +```swift +enum NetworkError: Error { + case invalidResponse + case decodingFailed(DecodingError) + case requestFailed(URLError) +} + +func fetchData() async throws(NetworkError) -> Data { + do { + let (data, _) = try await URLSession.shared.data(from: url) + return data + } catch let error as URLError { + throw .requestFailed(error) + } catch { + throw .invalidResponse + } +} +``` + +Callers know exactly which errors to handle. + +## Migration Strategy + +When converting closure-based code: + +1. **Add new async method alongside old one** - keeps code compiling +2. **Update method signature** - add `async`, remove completion parameter +3. **Replace closure calls with await** - use URLSession async APIs +4. **Remove optional unwrapping** - async APIs return non-optional values +5. **Simplify error handling** - use do-catch instead of nested closures +6. **Return directly** - compiler enforces return values + +## Common Patterns + +### Sequential execution (when order matters) + +```swift +let user = try await fetchUser(id: 1) +let posts = try await fetchPosts(userId: user.id) +let comments = try await fetchComments(postIds: posts.map(\.id)) +``` + +### Parallel execution (when independent) + +```swift +async let user = fetchUser(id: 1) +async let settings = fetchSettings() +async let notifications = fetchNotifications() + +let (userData, settingsData, notificationsData) = try await (user, settings, notifications) +``` + +### Mixed execution + +```swift +// Fetch user first (required for next step) +let user = try await fetchUser(id: 1) + +// Then fetch related data in parallel +async let posts = fetchPosts(userId: user.id) +async let followers = fetchFollowers(userId: user.id) +async let following = fetchFollowing(userId: user.id) + +let profile = Profile( + user: user, + posts: try await posts, + followers: try await followers, + following: try await following +) +``` + +## Further Learning + +For in-depth coverage of async/await patterns, error handling strategies, and real-world migration scenarios, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/async-sequences.md b/.claude/skills/swift-concurrency/references/async-sequences.md new file mode 100644 index 0000000..687715c --- /dev/null +++ b/.claude/skills/swift-concurrency/references/async-sequences.md @@ -0,0 +1,635 @@ +# Async Sequences and Streams + +Patterns for iterating over values that arrive over time. + +## AsyncSequence + +Protocol for asynchronous iteration over values that become available over time. + +### Basic usage + +```swift +for await value in someAsyncSequence { + print(value) +} +``` + +**Key difference from Sequence**: Values may not all be available immediately. + +### Custom implementation + +```swift +struct Counter: AsyncSequence, AsyncIteratorProtocol { + typealias Element = Int + + let limit: Int + var current = 1 + + mutating func next() async -> Int? { + guard !Task.isCancelled else { return nil } + guard current <= limit else { return nil } + + let result = current + current += 1 + return result + } + + func makeAsyncIterator() -> Counter { + self + } +} + +// Usage +for await count in Counter(limit: 5) { + print(count) // 1, 2, 3, 4, 5 +} +``` + +### Standard operators + +Same functional operators as regular sequences: + +```swift +// Filter +for await even in Counter(limit: 5).filter({ $0 % 2 == 0 }) { + print(even) // 2, 4 +} + +// Map +let mapped = Counter(limit: 5).map { $0 % 2 == 0 ? "Even" : "Odd" } +for await label in mapped { + print(label) +} + +// Contains (awaits until found or sequence ends) +let contains = await Counter(limit: 5).contains(3) // true +``` + +### Termination + +Return `nil` from `next()` to end iteration: + +```swift +mutating func next() async -> Int? { + guard !Task.isCancelled else { + return nil // Stop on cancellation + } + + guard current <= limit else { + return nil // Stop at limit + } + + return current +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 6.1: Working with asynchronous sequences](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## AsyncStream + +Convenient way to create async sequences without implementing protocols. + +### Basic creation + +```swift +let stream = AsyncStream { continuation in + for i in 1...5 { + continuation.yield(i) + } + continuation.finish() +} + +for await value in stream { + print(value) +} +``` + +### AsyncThrowingStream + +For streams that can fail: + +```swift +let throwingStream = AsyncThrowingStream { continuation in + continuation.yield(1) + continuation.yield(2) + continuation.finish(throwing: SomeError()) +} + +do { + for try await value in throwingStream { + print(value) + } +} catch { + print("Error: \(error)") +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 6.2: Using AsyncStream and AsyncThrowingStream in your code](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Bridging Closures to Streams + +### Progress + completion handlers + +```swift +// Old closure-based API +struct FileDownloader { + enum Status { + case downloading(Float) + case finished(Data) + } + + func download( + _ url: URL, + progressHandler: @escaping (Float) -> Void, + completion: @escaping (Result) -> Void + ) throws { + // Implementation + } +} + +// Modern stream-based API +extension FileDownloader { + func download(_ url: URL) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + do { + try self.download(url, progressHandler: { progress in + continuation.yield(.downloading(progress)) + }, completion: { result in + switch result { + case .success(let data): + continuation.yield(.finished(data)) + continuation.finish() + case .failure(let error): + continuation.finish(throwing: error) + } + }) + } catch { + continuation.finish(throwing: error) + } + } + } +} + +// Usage +for try await status in downloader.download(url) { + switch status { + case .downloading(let progress): + print("Progress: \(progress)") + case .finished(let data): + print("Done: \(data.count) bytes") + } +} +``` + +### Simplified with Result + +```swift +AsyncThrowingStream { continuation in + try self.download(url, progressHandler: { progress in + continuation.yield(.downloading(progress)) + }, completion: { result in + continuation.yield(with: result.map { .finished($0) }) + continuation.finish() + }) +} +``` + +## Bridging Delegates + +### Location updates example + +```swift +final class LocationMonitor: NSObject { + private var continuation: AsyncThrowingStream.Continuation? + let stream: AsyncThrowingStream + + override init() { + var capturedContinuation: AsyncThrowingStream.Continuation? + stream = AsyncThrowingStream { continuation in + capturedContinuation = continuation + } + super.init() + self.continuation = capturedContinuation + + locationManager.delegate = self + locationManager.startUpdatingLocation() + } +} + +extension LocationMonitor: CLLocationManagerDelegate { + func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { + for location in locations { + continuation?.yield(location) + } + } + + func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { + continuation?.finish(throwing: error) + } +} + +// Usage +let monitor = LocationMonitor() +for try await location in monitor.stream { + print("Location: \(location.coordinate)") +} +``` + +## Stream Lifecycle + +### Termination callback + +```swift +AsyncThrowingStream { continuation in + continuation.onTermination = { @Sendable reason in + print("Terminated: \(reason)") + // Cleanup: remove observers, cancel work, etc. + } + + continuation.yield(1) + continuation.finish() +} +``` + +**Termination reasons**: +- `.finished` - Normal completion +- `.finished(Error?)` - Completed with error (throwing stream) +- `.cancelled` - Task canceled + +### Cancellation + +Streams cancel when: +- Enclosing task cancels +- Stream goes out of scope + +```swift +let task = Task { + for try await status in download(url) { + print(status) + } +} + +task.cancel() // Triggers onTermination with .cancelled +``` + +**No explicit cancel method** - rely on task cancellation. + +## Buffer Policies + +Control what happens to values when no one is awaiting: + +### .unbounded (default) + +Buffers all values until consumed: + +```swift +let stream = AsyncStream { continuation in + (0...5).forEach { continuation.yield($0) } + continuation.finish() +} + +try await Task.sleep(for: .seconds(1)) + +for await value in stream { + print(value) // Prints all: 0, 1, 2, 3, 4, 5 +} +``` + +### .bufferingNewest(n) + +Keeps only the newest N values: + +```swift +let stream = AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + (0...5).forEach { continuation.yield($0) } + continuation.finish() +} + +try await Task.sleep(for: .seconds(1)) + +for await value in stream { + print(value) // Prints only: 5 +} +``` + +### .bufferingOldest(n) + +Keeps only the oldest N values: + +```swift +let stream = AsyncStream(bufferingPolicy: .bufferingOldest(1)) { continuation in + (0...5).forEach { continuation.yield($0) } + continuation.finish() +} + +try await Task.sleep(for: .seconds(1)) + +for await value in stream { + print(value) // Prints only: 0 +} +``` + +### .bufferingNewest(0) + +Only receives values emitted after iteration starts: + +```swift +let stream = AsyncStream(bufferingPolicy: .bufferingNewest(0)) { continuation in + continuation.yield(1) // Discarded + + Task { + try await Task.sleep(for: .seconds(2)) + continuation.yield(2) // Received + continuation.finish() + } +} + +try await Task.sleep(for: .seconds(1)) + +for await value in stream { + print(value) // Prints only: 2 +} +``` + +**Use case**: Location updates, file system changes - only care about latest. + +## Repeated Async Calls + +Use `init(unfolding:onCancel:)` for polling: + +```swift +struct PingService { + func startPinging() -> AsyncStream { + AsyncStream { + try? await Task.sleep(for: .seconds(5)) + return await ping() + } onCancel: { + print("Pinging cancelled") + } + } + + func ping() async -> Bool { + // Network request + return true + } +} + +// Usage +for await result in pingService.startPinging() { + print("Ping: \(result)") +} +``` + +## Standard Library Integration + +### NotificationCenter + +```swift +let stream = NotificationCenter.default.notifications( + named: .NSSystemTimeZoneDidChange +) + +for await notification in stream { + print("Time zone changed") +} +``` + +### Combine publishers + +```swift +let numbers = [1, 2, 3, 4, 5] +let filtered = numbers.publisher.filter { $0 % 2 == 0 } + +for await number in filtered.values { + print(number) // 2, 4 +} +``` + +### Task groups + +```swift +await withTaskGroup(of: Image.self) { group in + for url in urls { + group.addTask { await download(url) } + } + + for await image in group { + display(image) + } +} +``` + +## Limitations + +### Single consumer only + +Unlike Combine, streams support one consumer at a time: + +```swift +let stream = AsyncStream { continuation in + (0...5).forEach { continuation.yield($0) } + continuation.finish() +} + +Task { + for await value in stream { + print("Consumer 1: \(value)") + } +} + +Task { + for await value in stream { + print("Consumer 2: \(value)") + } +} + +// Unpredictable output - values split between consumers +// Consumer 1: 0 +// Consumer 2: 1 +// Consumer 1: 2 +// Consumer 2: 3 +``` + +**Solution**: Create separate streams or use third-party libraries (AsyncExtensions). + +### No values after termination + +Once finished, stream won't emit new values: + +```swift +let stream = AsyncStream { continuation in + continuation.finish() // Terminate immediately + continuation.yield(1) // Never received +} + +for await value in stream { + print(value) // Loop exits immediately +} +``` + +## Decision Guide + +### Use AsyncSequence when: + +- Implementing standard library-style protocols +- Need fine-grained control over iteration +- Building reusable sequence types +- Working with existing sequence protocols + +**Reality**: Rarely needed in application code. + +### Use AsyncStream when: + +- Bridging delegates to async/await +- Converting closure-based APIs +- Emitting events manually +- Polling or repeated async operations +- Most common use case + +### Use regular async methods when: + +- Single value returned +- No progress updates needed +- Simple request/response pattern + +```swift +// Use this +func fetchData() async throws -> Data + +// Not this +func fetchData() -> AsyncThrowingStream + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 6.3: Deciding between AsyncSequence, AsyncStream, or regular asynchronous methods](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) +``` + +## Common Patterns + +### Progress reporting + +```swift +func download(_ url: URL) -> AsyncThrowingStream { + AsyncThrowingStream { continuation in + Task { + do { + var progress: Double = 0 + while progress < 1.0 { + progress += 0.1 + continuation.yield(.progress(progress)) + try await Task.sleep(for: .milliseconds(100)) + } + + let data = try await URLSession.shared.data(from: url).0 + continuation.yield(.completed(data)) + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } +} +``` + +### Monitoring file system + +```swift +func watchDirectory(_ path: String) -> AsyncStream { + AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + let source = DispatchSource.makeFileSystemObjectSource( + fileDescriptor: fd, + eventMask: .write, + queue: .main + ) + + source.setEventHandler { + continuation.yield(.fileChanged(path)) + } + + continuation.onTermination = { _ in + source.cancel() + } + + source.resume() + } +} +``` + +### Timer/polling + +```swift +func timer(interval: Duration) -> AsyncStream { + AsyncStream { continuation in + Task { + while !Task.isCancelled { + continuation.yield(Date()) + try? await Task.sleep(for: interval) + } + continuation.finish() + } + } +} + +// Usage +for await date in timer(interval: .seconds(1)) { + print("Tick: \(date)") +} +``` + +## Best Practices + +1. **Always call finish()** - Streams stay alive until terminated +2. **Use buffer policies wisely** - Match your use case (latest value vs all values) +3. **Handle cancellation** - Set `onTermination` for cleanup +4. **Single consumer** - Don't share streams across multiple consumers +5. **Prefer streams over closures** - More composable and cancellable +6. **Check Task.isCancelled** - Respect cancellation in custom sequences +7. **Use throwing variant** - When operations can fail +8. **Consider regular async** - If only returning single value + +## Debugging + +### Add termination logging + +```swift +continuation.onTermination = { reason in + print("Stream ended: \(reason)") +} +``` + +### Validate finish() calls + +```swift +// ❌ Forgot to finish +AsyncStream { continuation in + continuation.yield(1) + // Stream never ends! +} + +// ✅ Always finish +AsyncStream { continuation in + continuation.yield(1) + continuation.finish() +} +``` + +### Check for dropped values + +```swift +let stream = AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in + for i in 1...100 { + continuation.yield(i) + print("Yielded: \(i)") + } + continuation.finish() +} + +// If consumer is slow, many values dropped +for await value in stream { + print("Received: \(value)") + try? await Task.sleep(for: .seconds(1)) +} +``` + +## Further Learning + +For real-world migration examples, performance patterns, and advanced stream techniques, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/core-data.md b/.claude/skills/swift-concurrency/references/core-data.md new file mode 100644 index 0000000..2611b22 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/core-data.md @@ -0,0 +1,533 @@ +# Core Data and Swift Concurrency + +Thread-safe patterns for using Core Data with Swift Concurrency. + +## Core Principles + +### Thread safety still matters + +Core Data's thread safety rules don't change with Swift Concurrency: +- Can't pass `NSManagedObject` between threads +- Must access objects on their context's thread +- `NSManagedObjectID` is thread-safe (can pass around) + +### NSManagedObject cannot be Sendable + +```swift +@objc(Article) +public class Article: NSManagedObject { + @NSManaged public var title: String // ❌ Mutable, can't be Sendable +} +``` + +**Don't use `@unchecked Sendable`** - hides warnings without fixing safety. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.1: An introduction to Swift Concurrency and Core Data](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Available Async APIs + +### Context perform + +```swift +extension NSManagedObjectContext { + func perform( + schedule: ScheduledTaskType = .immediate, + _ block: @escaping () throws -> T + ) async rethrows -> T +} +``` + +### What's missing + +No async alternative for: +```swift +func loadPersistentStores( + completionHandler: @escaping (NSPersistentStoreDescription, Error?) -> Void +) +``` + +Must bridge manually (see below). + +## Data Access Objects (DAO) + +Thread-safe value types representing managed objects. + +### Pattern + +```swift +// 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 + } +} +``` + +### Benefits + +- **Sendable**: Safe to pass across isolation domains +- **Immutable**: No accidental mutations +- **Clear API**: Explicit data transfer + +### Drawbacks + +- **Requires rewrite**: All fetch/mutation logic +- **Boilerplate**: DAO for each entity +- **Complexity**: Additional layer of abstraction + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.2: Sendable and NSManageObjects](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Working Without DAOs + +Pass only `NSManagedObjectID` between contexts. + +### Basic pattern + +```swift +@MainActor +func fetchArticle(id: NSManagedObjectID) -> Article? { + viewContext.object(with: id) as? Article +} + +func processInBackground(articleID: NSManagedObjectID) async throws { + let backgroundContext = container.newBackgroundContext() + try await backgroundContext.perform { + guard let article = backgroundContext.object(with: articleID) as? Article else { + return + } + // Process article + try backgroundContext.save() + } +} +``` + +### NSManagedObjectID is Sendable + +```swift +// Safe to pass between tasks +let articleID = article.objectID + +Task { + await processInBackground(articleID: articleID) +} +``` + +## Bridging Closures to Async + +### Load persistent stores + +```swift +extension NSPersistentContainer { + func loadPersistentStores() async throws { + try await withCheckedThrowingContinuation { continuation in + self.loadPersistentStores { description, error in + if let error { + continuation.resume(throwing: error) + } else { + continuation.resume(returning: ()) + } + } + } + } +} + +// Usage +try await container.loadPersistentStores() +``` + +## Simple CoreDataStore Pattern + +Enforce isolation at API level: + +```swift +nonisolated struct CoreDataStore { + static let shared = CoreDataStore() + + let persistentContainer: NSPersistentContainer + private var viewContext: NSManagedObjectContext { + persistentContainer.viewContext + } + + private init() { + persistentContainer = NSPersistentContainer(name: "MyApp") + persistentContainer.viewContext.automaticallyMergesChangesFromParent = true + + Task { [persistentContainer] in + try? await persistentContainer.loadPersistentStores() + } + } + + // View context operations (main thread) + @MainActor + func perform(_ block: (NSManagedObjectContext) throws -> Void) rethrows { + try block(viewContext) + } + + // Background operations + @concurrent + func performInBackground( + _ block: @escaping (NSManagedObjectContext) throws -> T + ) async rethrows -> T { + let context = persistentContainer.newBackgroundContext() + return try await context.perform { + try block(context) + } + } +} +``` + +### Usage + +```swift +// Main thread operations +@MainActor +func loadArticles() throws -> [Article] { + try CoreDataStore.shared.perform { context in + let request = Article.fetchRequest() + return try context.fetch(request) + } +} + +// Background operations +func deleteAll() async throws { + try await CoreDataStore.shared.performInBackground { context in + let request = Article.fetchRequest() + let articles = try context.fetch(request) + articles.forEach { context.delete($0) } + try context.save() + } +} +``` + +### Why this pattern works + +- **@MainActor**: Enforces view context on main thread +- **@concurrent**: Forces background execution +- **Compile-time safety**: Wrong isolation = error +- **Simple**: No custom executors needed + +## Custom Actor Executor (Advanced) + +**Note**: Usually not needed. Consider simple pattern first. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.3: Using a custom Actor executor for Core Data (advanced)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Implementation + +```swift +final class NSManagedObjectContextExecutor: @unchecked Sendable, SerialExecutor { + private let context: NSManagedObjectContext + + init(context: NSManagedObjectContext) { + self.context = context + } + + func enqueue(_ job: consuming ExecutorJob) { + let unownedJob = UnownedJob(job) + let executor = asUnownedSerialExecutor() + + context.perform { + unownedJob.runSynchronously(on: executor) + } + } + + func asUnownedSerialExecutor() -> UnownedSerialExecutor { + UnownedSerialExecutor(ordinary: self) + } +} +``` + +### Actor usage + +```swift +actor CoreDataStore { + let persistentContainer: NSPersistentContainer + nonisolated let modelExecutor: NSManagedObjectContextExecutor + + nonisolated var unownedExecutor: UnownedSerialExecutor { + modelExecutor.asUnownedSerialExecutor() + } + + private init() { + persistentContainer = NSPersistentContainer(name: "MyApp") + let context = persistentContainer.newBackgroundContext() + modelExecutor = NSManagedObjectContextExecutor(context: context) + } + + func deleteAll( + using request: NSFetchRequest + ) throws { + let objects = try context.fetch(request) + objects.forEach { context.delete($0) } + try context.save() + } +} +``` + +### Drawbacks + +- **Hidden complexity**: Executor details obscure Core Data +- **Forces concurrency**: Even for main thread operations +- **Not simpler**: More code than `perform { }` +- **Error prone**: Easy to use wrong context + +**Recommendation**: Use simple pattern instead. + +## Default MainActor Isolation + +### Problem with auto-generated code + +When default isolation set to `@MainActor`, auto-generated managed objects conflict: + +```swift +// Auto-generated (can't modify) +class Article: NSManagedObject { + // Inherits @MainActor, conflicts with NSManagedObject +} +``` + +**Error**: `Main actor-isolated initializer has different actor isolation from nonisolated overridden declaration` + +### Solution: Manual code generation + +1. Set entity to "Manual/None" code generation +2. Generate class definitions +3. Mark as `nonisolated`: + +```swift +nonisolated class Article: NSManagedObject { + @NSManaged public var title: String? + @NSManaged public var timestamp: Date? +} + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 9.4: Autogenerated Core Data Objects and Default MainActor Isolation Conflicts](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) +``` + +**Benefit**: Full control over isolation. + +## Common Patterns + +### Fetch on main thread + +```swift +@MainActor +func fetchArticles() throws -> [Article] { + let request = Article.fetchRequest() + return try viewContext.fetch(request) +} +``` + +### Background save + +```swift +func saveInBackground() async throws { + let context = container.newBackgroundContext() + try await context.perform { + let article = Article(context: context) + article.title = "New Article" + try context.save() + } +} +``` + +### Pass ID, fetch in context + +```swift +@MainActor +func displayArticle(id: NSManagedObjectID) { + guard let article = viewContext.object(with: id) as? Article else { + return + } + // Use article +} + +func processArticle(id: NSManagedObjectID) async throws { + try await CoreDataStore.shared.performInBackground { context in + guard let article = context.object(with: id) as? Article else { + return + } + // Process article + try context.save() + } +} +``` + +### Batch operations + +```swift +@concurrent +func deleteAllArticles() async throws { + try await CoreDataStore.shared.performInBackground { context in + let request = NSFetchRequest(entityName: "Article") + let deleteRequest = NSBatchDeleteRequest(fetchRequest: request) + try context.execute(deleteRequest) + } +} +``` + +## SwiftUI Integration + +### Environment injection + +```swift +@main +struct MyApp: App { + let persistentContainer = NSPersistentContainer(name: "MyApp") + + var body: some Scene { + WindowGroup { + ContentView() + .environment(\.managedObjectContext, persistentContainer.viewContext) + } + } +} +``` + +### View usage + +```swift +struct ContentView: View { + @Environment(\.managedObjectContext) private var viewContext + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Article.timestamp, ascending: true)] + ) private var articles: FetchedResults
+ + var body: some View { + List(articles) { article in + Text(article.title ?? "") + } + } +} +``` + +## Best Practices + +1. **Pass NSManagedObjectID only** - never managed objects +2. **Use perform { }** - don't access context directly +3. **@MainActor for view context** - enforce main thread +4. **@concurrent for background** - force background execution +5. **Manual code generation** - control isolation +6. **Keep it simple** - avoid custom executors unless needed +7. **Enable Core Data debugging** - catch thread violations +8. **Merge changes automatically** - `automaticallyMergesChangesFromParent = true` +9. **Use background contexts** - for heavy operations +10. **Test with Thread Sanitizer** - catch violations early + +## Debugging + +### Enable Core Data concurrency debugging + +```swift +// Launch argument +-com.apple.CoreData.ConcurrencyDebug 1 +``` + +Crashes immediately on thread violations. + +### Thread Sanitizer + +Enable in scheme settings to catch data races. + +### Assertions + +```swift +@MainActor +func fetchArticles() -> [Article] { + assert(Thread.isMainThread) + // Fetch from viewContext +} +``` + +## Decision Tree + +``` +Need to access Core Data? +├─ UI/View context? +│ └─ Use @MainActor + viewContext +│ +├─ Background operation? +│ ├─ Quick operation? → perform { } on background context +│ └─ Batch operation? → NSBatchDeleteRequest/NSBatchUpdateRequest +│ +├─ Pass between contexts? +│ └─ Use NSManagedObjectID only +│ +└─ Need Sendable type? + ├─ Can refactor? → Use DAO pattern + └─ Can't refactor? → Pass NSManagedObjectID +``` + +## Migration Strategy + +### For existing projects + +1. **Enable manual code generation** for all entities +2. **Mark entities as nonisolated** if using default @MainActor +3. **Wrap Core Data access** in CoreDataStore +4. **Use @MainActor** for view context operations +5. **Use @concurrent** for background operations +6. **Pass NSManagedObjectID** between contexts +7. **Test with debugging enabled** + +### For new projects + +1. **Start with simple pattern** (CoreDataStore) +2. **Manual code generation** from the start +3. **Consider DAOs** if heavy cross-context usage +4. **Enable strict concurrency** early + +## Common Mistakes + +### ❌ Passing managed objects + +```swift +func process(article: Article) async { + // ❌ Article not Sendable +} +``` + +### ❌ Accessing context from wrong thread + +```swift +func background() async { + let articles = viewContext.fetch(request) // ❌ Not on main thread +} +``` + +### ❌ Using @unchecked Sendable + +```swift +extension Article: @unchecked Sendable {} // ❌ Doesn't make it safe +``` + +### ❌ Not using perform + +```swift +func save() async { + backgroundContext.save() // ❌ Not on context's thread +} +``` + +## Further Learning + +For Core Data best practices, migration strategies, and advanced patterns: +- [Core Data Best Practices](https://github.com/avanderlee/CoreDataBestPractices) +- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com) + diff --git a/.claude/skills/swift-concurrency/references/glossary.md b/.claude/skills/swift-concurrency/references/glossary.md new file mode 100644 index 0000000..8bd00cb --- /dev/null +++ b/.claude/skills/swift-concurrency/references/glossary.md @@ -0,0 +1,57 @@ +# Glossary + +Concise definitions of key Swift Concurrency terms used throughout this skill. + +## Actor isolation + +A rule enforced by the compiler: actor-isolated state can only be accessed from the actor's executor. Cross-actor access requires `await`. + +## Global actor + +A shared isolation domain applied via attributes like `@MainActor` or a custom `@globalActor`. Types/functions isolated to the same global actor can interact without crossing isolation. + +## Default actor isolation + +A module/target-level setting that changes the default isolation of declarations. App targets often choose `@MainActor` as the default to reduce migration noise, but it changes behavior and diagnostics. + +## Strict concurrency checking + +Compiler enforcement levels for Sendable and isolation diagnostics (minimal/targeted/complete). Raising the level typically reveals more issues and can trigger the “concurrency rabbit hole” unless migrated incrementally. + +## Sendable + +A marker protocol that indicates a type is safe to transfer across isolation boundaries. The compiler verifies stored properties and captured values for thread-safety. + +## @Sendable + +An annotation for function types/closures that can be executed concurrently. It tightens capture rules (captured values must be Sendable or safely transferred). + +## Suspension point + +An `await` site where a task may suspend and later resume. After a suspension point, you must assume other work may have run and (for actors) state may have changed (reentrancy). + +## Reentrancy (actors) + +While an actor is suspended at an `await`, other tasks can enter the actor and mutate state. Code after `await` must not assume actor state is unchanged. + +## nonisolated + +Marks a declaration as not isolated to the surrounding actor/global actor. Use only when it truly does not touch isolated mutable state (typically immutable Sendable data). + +## nonisolated(nonsending) (Swift 6.2+ behavior) + +An opt-out to prevent “sending” non-Sendable values across isolation while still allowing an async function to run in the caller’s isolation. Used to reduce Sendable friction when you do not need to hop executors. + +## @concurrent (Swift 6.2+ behavior) + +An attribute used to explicitly opt a nonisolated async function into concurrent execution (i.e., not inheriting the caller’s actor). It is used during migration when enabling `NonisolatedNonsendingByDefault`. + +## @preconcurrency + +An annotation used to suppress Sendable-related diagnostics from a module that predates concurrency annotations. It reduces noise but shifts safety responsibility to you. + +## Region-based isolation / sending + +Mechanisms that model ownership transfer so certain non-Sendable values can be moved between regions safely. The `sending` keyword enforces that a value is no longer used after transfer. + + diff --git a/.claude/skills/swift-concurrency/references/linting.md b/.claude/skills/swift-concurrency/references/linting.md new file mode 100644 index 0000000..056071c --- /dev/null +++ b/.claude/skills/swift-concurrency/references/linting.md @@ -0,0 +1,38 @@ +# Linting & Concurrency + +Guidance for handling lint rules related to Swift Concurrency. + +## SwiftLint: `async_without_await` +- **Intent**: A declaration should not be `async` if it never awaits. +- **Never “fix”** by inserting fake suspension (e.g. `await Task.yield()`, `await Task { ... }.value`). Those mask the real issue and add meaningless suspension points. +- **Legit use of `Task.yield()`**: OK in tests or scheduling control when you truly need a yield; not as a lint workaround. + +### Diagnose why the declaration is `async` +1) **Protocol requirement** — the protocol method/property is `async`. +2) **Override requirement** — base class API is `async`. +3) **`@concurrent` requirement** — stays `async` even without `await`. +4) **Accidental/legacy `async`** — no caller needs async semantics. + +### Preferred fixes (order) +1) **Remove `async`** (and adjust call sites) when no async semantics are needed. +2) If `async` is required (protocol/override/@concurrent): + - Re-evaluate the upstream API if you own it (can it be non-async?). + - If you cannot change it, keep `async` and **narrowly suppress the rule** where appropriate (common for mocks/stubs/overrides). + +### Suppression examples (keep scope tight) +```swift +// swiftlint:disable:next async_without_await +func fetch() async { perform() } + +// For a block: +// swiftlint:disable async_without_await +func makeMock() async { perform() } +// swiftlint:enable async_without_await +``` + +### Quick checklist +- [ ] Confirm if `async` is truly required (protocol/override/@concurrent). +- [ ] If not required, remove `async` and update callers. +- [ ] If required, prefer localized suppression over dummy awaits. +- [ ] Avoid adding new suspension points without intent. + diff --git a/.claude/skills/swift-concurrency/references/memory-management.md b/.claude/skills/swift-concurrency/references/memory-management.md new file mode 100644 index 0000000..67b0ac4 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/memory-management.md @@ -0,0 +1,542 @@ +# Memory Management + +Preventing retain cycles and managing object lifetimes in Swift Concurrency. + +## Core Concepts + +### Tasks capture like closures + +Tasks capture variables and references just like regular closures. Swift doesn't automatically prevent retain cycles in concurrent code. + +```swift +Task { + self.doWork() // ⚠️ Strong capture of self +} +``` + +### Why concurrency hides memory issues + +- Tasks may live longer than expected +- Async operations delay execution +- Harder to track when memory should be released +- Long-running tasks can hold references indefinitely + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 8.1: Overview of memory management in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Retain Cycles + +### What is a retain cycle? + +Two or more objects hold strong references to each other, preventing deallocation. + +```swift +class A { + var b: B? +} + +class B { + var a: A? +} + +let a = A() +let b = B() +a.b = b +b.a = a // Retain cycle - neither can be deallocated +``` + +### Retain cycles with Tasks + +When task captures `self` strongly and `self` owns the task: + +```swift +@MainActor +final class ImageLoader { + var task: Task? + + func startPolling() { + task = Task { + while true { + self.pollImages() // ⚠️ Strong capture + try? await Task.sleep(for: .seconds(1)) + } + } + } +} + +var loader: ImageLoader? = .init() +loader?.startPolling() +loader = nil // ⚠️ Loader never deallocated - retain cycle! +``` + +**Problem**: Task holds `self`, `self` holds task → neither released. + +## Breaking Retain Cycles + +### Use weak self + +```swift +func startPolling() { + task = Task { [weak self] in + while let self = self { + self.pollImages() + try? await Task.sleep(for: .seconds(1)) + } + } +} + +var loader: ImageLoader? = .init() +loader?.startPolling() +loader = nil // ✅ Loader deallocated, task stops +``` + +### Pattern for long-running tasks + +```swift +task = Task { [weak self] in + while let self = self { + await self.doWork() + try? await Task.sleep(for: interval) + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 8.2: Preventing retain cycles when using Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +Loop exits when `self` becomes `nil`. + +## One-Way Retention + +Task retains `self`, but `self` doesn't retain task. Object stays alive until task completes. + +```swift +@MainActor +final class ViewModel { + func fetchData() { + Task { + await performRequest() + updateUI() // ⚠️ Strong capture + } + } +} + +var viewModel: ViewModel? = .init() +viewModel?.fetchData() +viewModel = nil // ViewModel stays alive until task completes +``` + +**Execution order**: +1. Task starts +2. `viewModel = nil` (but object not deallocated) +3. Task completes +4. ViewModel finally deallocated + +### When one-way retention is acceptable + +Short-lived tasks that complete quickly: + +```swift +func saveData() { + Task { + await database.save(self.data) // OK - completes quickly + } +} +``` + +### When to use weak self + +Long-running or indefinite tasks: + +```swift +func startMonitoring() { + Task { [weak self] in + for await event in eventStream { + self?.handle(event) + } + } +} +``` + +## Async Sequences and Retention + +### Problem: Infinite sequences + +```swift +@MainActor +final class AppLifecycleViewModel { + private(set) var isActive = false + private var task: Task? + + func startObserving() { + task = Task { + for await _ in NotificationCenter.default.notifications( + named: .didBecomeActive + ) { + isActive = true // ⚠️ Strong capture, never ends + } + } + } +} + +var viewModel: AppLifecycleViewModel? = .init() +viewModel?.startObserving() +viewModel = nil // ⚠️ Never deallocated - sequence continues +``` + +**Problem**: Async sequence never finishes, task holds `self` indefinitely. + +### Solution 1: Manual cancellation + +```swift +func startObserving() { + task = Task { + for await _ in NotificationCenter.default.notifications( + named: .didBecomeActive + ) { + isActive = true + } + } +} + +func stopObserving() { + task?.cancel() +} + +// Usage +viewModel?.startObserving() +viewModel?.stopObserving() // Must call before release +viewModel = nil +``` + +### Solution 2: Weak self with guard + +```swift +func startObserving() { + task = Task { [weak self] in + for await _ in NotificationCenter.default.notifications( + named: .didBecomeActive + ) { + guard let self = self else { return } + self.isActive = true + } + } +} +``` + +Task exits when `self` deallocates. + +## Isolated deinit (Swift 6.2+) + +Clean up actor-isolated state in deinit: + +```swift +@MainActor +final class ViewModel { + private var task: Task? + + isolated deinit { + task?.cancel() + } +} +``` + +**Limitation**: Won't break retain cycles (deinit never called if cycle exists). + +**Use for**: Cleanup when object is being deallocated normally. + +## Common Patterns + +### Short-lived task (strong capture OK) + +```swift +func saveData() { + Task { + await database.save(self.data) + self.updateUI() + } +} +``` + +**When safe**: Task completes quickly, acceptable for object to live until done. + +### Long-running task (weak self required) + +```swift +func startPolling() { + task = Task { [weak self] in + while let self = self { + await self.fetchUpdates() + try? await Task.sleep(for: .seconds(5)) + } + } +} +``` + +### Async sequence monitoring (weak self + guard) + +```swift +func startMonitoring() { + task = Task { [weak self] in + for await event in eventStream { + guard let self = self else { return } + self.handle(event) + } + } +} +``` + +### Cancellable work with cleanup + +```swift +func startWork() { + task = Task { [weak self] in + defer { self?.cleanup() } + + while let self = self { + await self.doWork() + try? await Task.sleep(for: .seconds(1)) + } + } +} +``` + +## Detection Strategies + +### Add deinit logging + +```swift +deinit { + print("✅ \(type(of: self)) deallocated") +} +``` + +If deinit never prints → likely retain cycle. + +### Memory graph debugger + +1. Run app in Xcode +2. Debug → Debug Memory Graph +3. Look for cycles in object graph + +### Instruments + +Use Leaks instrument to detect retain cycles at runtime. + +## Decision Tree + +``` +Task captures self? +├─ Task completes quickly? +│ └─ Strong capture OK +│ +├─ Long-running or infinite? +│ ├─ Can use weak self? → Use [weak self] +│ ├─ Need manual control? → Store task, cancel explicitly +│ └─ Async sequence? → [weak self] + guard +│ +└─ Self owns task? + ├─ Yes → High risk of retain cycle + └─ No → Lower risk, but check lifetime +``` + +## Best Practices + +1. **Default to weak self** for long-running tasks +2. **Use guard let self** in async sequences +3. **Cancel tasks explicitly** when possible +4. **Add deinit logging** during development +5. **Test object deallocation** in unit tests +6. **Use Memory Graph** to verify no cycles +7. **Document lifetime expectations** in comments +8. **Prefer cancellation** over weak self when possible +9. **Avoid nested strong captures** in task closures +10. **Use isolated deinit** for cleanup (Swift 6.2+) + +## Testing for Leaks + +### Unit test pattern + +```swift +func testViewModelDeallocates() async { + var viewModel: ViewModel? = ViewModel() + weak var weakViewModel = viewModel + + viewModel?.startWork() + viewModel = nil + + // Give tasks time to complete + try? await Task.sleep(for: .milliseconds(100)) + + XCTAssertNil(weakViewModel, "ViewModel should be deallocated") +} +``` + +### SwiftUI view test + +```swift +func testViewDeallocates() { + var view: MyView? = MyView() + weak var weakView = view + + view = nil + + XCTAssertNil(weakView) +} +``` + +## Common Mistakes + +### ❌ Forgetting weak self in loops + +```swift +Task { + while true { + self.poll() // Retain cycle + try? await Task.sleep(for: .seconds(1)) + } +} +``` + +### ❌ Strong capture in async sequences + +```swift +Task { + for await item in stream { + self.process(item) // May never release + } +} +``` + +### ❌ Not canceling stored tasks + +```swift +class Manager { + var task: Task? + + func start() { + task = Task { + await self.work() // Retain cycle + } + } + + // Missing: deinit { task?.cancel() } +} +``` + +### ❌ Assuming deinit breaks cycles + +```swift +deinit { + task?.cancel() // Never called if retain cycle exists +} +``` + +## Examples by Use Case + +### Polling service + +```swift +final class PollingService { + private var task: Task? + + func start() { + task = Task { [weak self] in + while let self = self { + await self.poll() + try? await Task.sleep(for: .seconds(5)) + } + } + } + + func stop() { + task?.cancel() + } +} +``` + +### Notification observer + +```swift +@MainActor +final class NotificationObserver { + private var task: Task? + + func startObserving() { + task = Task { [weak self] in + for await notification in NotificationCenter.default.notifications( + named: .someNotification + ) { + guard let self = self else { return } + self.handle(notification) + } + } + } + + isolated deinit { + task?.cancel() + } +} +``` + +### Download manager + +```swift +final class DownloadManager { + private var tasks: [URL: Task] = [:] + + func download(_ url: URL) async throws -> Data { + let task = Task { [weak self] in + defer { self?.tasks.removeValue(forKey: url) } + return try await URLSession.shared.data(from: url).0 + } + + tasks[url] = task + return try await task.value + } + + func cancelAll() { + tasks.values.forEach { $0.cancel() } + tasks.removeAll() + } +} +``` + +### Timer + +```swift +actor Timer { + private var task: Task? + + func start(interval: Duration, action: @Sendable () async -> Void) { + task = Task { + while !Task.isCancelled { + await action() + try? await Task.sleep(for: interval) + } + } + } + + func stop() { + task?.cancel() + } +} +``` + +## Debugging Checklist + +When object won't deallocate: + +- [ ] Check for strong self captures in tasks +- [ ] Verify tasks are canceled or complete +- [ ] Look for infinite loops or sequences +- [ ] Check if self owns the task +- [ ] Use Memory Graph to find cycles +- [ ] Add deinit logging to verify +- [ ] Test with weak references +- [ ] Review async sequence usage +- [ ] Check nested task captures +- [ ] Verify cleanup in deinit + +## Further Learning + +For migration strategies, real-world examples, and advanced memory patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/migration.md b/.claude/skills/swift-concurrency/references/migration.md new file mode 100644 index 0000000..806d6e8 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/migration.md @@ -0,0 +1,721 @@ +# Migration to Swift 6 and Strict Concurrency + +A practical guide to migrating existing Swift codebases to Swift 6's strict concurrency model, including strategies, habits, tooling, and common patterns. + +--- + +## Why Migrate to Swift 6? + +Swift 6 doesn't fundamentally change how Swift Concurrency works—it **enforces existing rules more strictly**: + +- **Compile-time safety**: Catches data races and threading issues at compile time instead of runtime +- **Warnings become errors**: Many Swift 5 warnings become hard errors in Swift 6 language mode +- **Future-proofing**: New concurrency features will build on this stricter foundation +- **Better maintainability**: Code becomes safer and easier to reason about + +> **Important**: You can adopt strict concurrency checking gradually while still compiling under Swift 5. You don't need to flip the Swift 6 switch immediately. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.2: The impact of Swift 6 on Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Project Settings That Change Concurrency Behavior + +Before interpreting diagnostics or choosing a fix, confirm the target/module settings. These settings can materially change how code executes and what the compiler enforces. + +### Quick matrix + +| Setting / feature | Where to check | Why it matters | +|---|---|---| +| Swift language mode (Swift 5.x vs Swift 6) | Xcode build settings (`SWIFT_VERSION`) / SwiftPM `// swift-tools-version:` | Swift 6 turns many warnings into errors and enables stricter defaults. | +| Strict concurrency checking | Xcode: Strict Concurrency Checking (`SWIFT_STRICT_CONCURRENCY`) / SwiftPM: strict concurrency flags | Controls how aggressively Sendable + isolation rules are enforced. | +| Default actor isolation | Xcode: Default Actor Isolation (`SWIFT_DEFAULT_ACTOR_ISOLATION`) / SwiftPM: `.defaultIsolation(MainActor.self)` | Changes the default isolation of declarations; can reduce migration noise but changes behavior and requirements. | +| `NonisolatedNonsendingByDefault` | Xcode upcoming feature / SwiftPM `.enableUpcomingFeature("NonisolatedNonsendingByDefault")` | Changes how nonisolated async functions execute (can inherit the caller’s actor unless explicitly marked `@concurrent`). | +| Approachable Concurrency | Xcode build setting / SwiftPM enables the underlying upcoming features | Bundles multiple upcoming features; recommended to migrate feature-by-feature first. | + +## The Concurrency Rabbit Hole + +A common migration experience: + +1. Enable strict concurrency checking +2. See 50+ errors and warnings +3. Fix a bunch of them +4. Rebuild and see 80+ new errors appear + +**Why this happens**: Fixing isolation in one place often exposes issues elsewhere. This is normal and manageable with the right strategy. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.1: Challenges in migrating to Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Six Migration Habits for Success + +### 1. Don't Panic—It's All About Iterations + +Break migration into small, manageable chunks: + +```swift +// Day 1: Enable strict concurrency, fix a few warnings +// Build Settings → Strict Concurrency Checking = Complete + +// Day 2: Fix more warnings + +// Day 3: Revert to minimal checking if needed +// Build Settings → Strict Concurrency Checking = Minimal +``` + +Allow yourself 30 minutes per day to migrate gradually. Don't expect completion in a few days for large projects. + +### 2. Sendable by Default for New Code + +When writing new types, make them `Sendable` from the start: + +```swift +// ✅ Good: New code prepared for Swift 6 +struct UserProfile: Sendable { + let id: UUID + let name: String +} + +// ❌ Avoid: Creating technical debt +class UserProfile { // Will need migration later + var id: UUID + var name: String +} +``` + +It's easier to design for concurrency upfront than to retrofit it later. + +### 3. Use Swift 6 for New Projects and Packages + +For new projects, packages, or files: +- Enable Swift 6 language mode from the start +- Use Swift Concurrency features (async/await, actors) +- Reduce technical debt before it accumulates + +You can enable Swift 6 for individual files in a Swift 5 project to prevent scope creep. + +### 4. Resist the Urge to Refactor + +**Focus solely on concurrency changes**. Don't combine migration with: +- Architecture refactors +- API modernization +- Code style improvements + +Create separate tickets for non-concurrency refactors and address them later. + +### 5. Focus on Minimal Changes + +- Make small, focused pull requests +- Migrate one class or module at a time +- Get changes merged quickly to create checkpoints +- Avoid large PRs that are hard to review + +### 6. Don't Just @MainActor All the Things + +Don't blindly add `@MainActor` to fix warnings. Consider: +- Should this actually run on the main actor? +- Would a custom actor be more appropriate? +- Is `nonisolated` the right choice? + +**Exception**: For app projects (not frameworks), consider enabling **Default Actor Isolation** to `@MainActor`, since most app code needs main thread access. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.3: The six migration habits for a successful migration](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Step-by-Step Migration Process + +### 1. Find an Isolated Piece of Code + +Start with: +- Standalone packages with minimal dependencies +- Individual Swift files within a package +- Code that's not heavily used throughout the project + +**Why**: Fewer dependencies = less risk of falling into the concurrency rabbit hole. + +### 2. Update Related Dependencies + +Before enabling strict concurrency: + +```swift +// Update third-party packages to latest versions +// Example: Vapor, Alamofire, etc. +``` + +Apply these updates in a separate PR before proceeding with concurrency changes. + +### 3. Add Async Alternatives + +Provide async/await wrappers for existing closure-based APIs: + +```swift +// Original closure-based API +@available(*, deprecated, renamed: "fetchImage(urlRequest:)", + message: "Consider using the async/await alternative.") +func fetchImage(urlRequest: URLRequest, + completion: @escaping @Sendable (Result) -> Void) { + // ... existing implementation +} + +// New async wrapper +func fetchImage(urlRequest: URLRequest) async throws -> UIImage { + return try await withCheckedThrowingContinuation { continuation in + fetchImage(urlRequest: urlRequest) { result in + continuation.resume(with: result) + } + } +} +``` + +**Benefits**: +- Colleagues can start using async/await immediately +- You can migrate callers before rewriting implementation +- Tests can be updated to async/await first + +**Tip**: Use Xcode's **Refactor → Add Async Wrapper** to generate these automatically. + +### 4. Change Default Actor Isolation (Swift 6.2+) + +For app projects, set default isolation to `@MainActor`: + +**Xcode Build Settings**: +``` +Swift Concurrency → Default Actor Isolation = MainActor +``` + +**Swift Package Manager**: +```swift +.target( + name: "MyTarget", + swiftSettings: [ + .defaultIsolation(MainActor.self) + ] +) +``` + +This drastically reduces warnings in app code where most types need main thread access. + +### 5. Enable Strict Concurrency Checking + +**Xcode Build Settings**: Search for "Strict Concurrency Checking" + +Three levels available: + +- **Minimal**: Only checks code that explicitly adopts concurrency (`@Sendable`, `@MainActor`) +- **Targeted**: Checks all code that adopts concurrency, including `Sendable` conformances +- **Complete**: Checks entire codebase (matches Swift 6 behavior) + +**Swift Package Manager**: +```swift +.target( + name: "MyTarget", + swiftSettings: [ + .enableExperimentalFeature("StrictConcurrency=targeted") + ] +) +``` + +**Strategy**: Start with Minimal → Targeted → Complete, fixing errors at each level. + +### 6. Add Sendable Conformances + +Even if the compiler doesn't complain, add `Sendable` to types that will cross isolation domains: + +```swift +// ✅ Prepare for future use +struct Configuration: Sendable { + let apiKey: String + let timeout: TimeInterval +} +``` + +This prevents warnings when the type is used in concurrent contexts later. + +### 7. Enable Approachable Concurrency (Swift 6.2+) + +**Xcode Build Settings**: Search for "Approachable Concurrency" + +Enables multiple upcoming features at once: +- `DisableOutwardActorInference` +- `GlobalActorIsolatedTypesUsability` +- `InferIsolatedConformances` +- `InferSendableFromCaptures` +- `NonisolatedNonsendingByDefault` + +**⚠️ Warning**: Don't just flip this switch for existing projects. Use migration tooling (see below) to migrate to each feature individually first. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.5: The Approachable Concurrency build setting (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### 8. Enable Upcoming Features + +**Xcode Build Settings**: Search for "Upcoming Feature" + +Enable features individually: + +**Swift Package Manager**: +```swift +.target( + name: "MyTarget", + swiftSettings: [ + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("InferIsolatedConformances") + ] +) +``` + +Find feature keys in Swift Evolution proposals (e.g., SE-335 for `ExistentialAny`). + +### 9. Change to Swift 6 Language Mode + +**Xcode Build Settings**: +``` +Swift Language Version = Swift 6 +``` + +**Swift Package Manager**: +```swift +// swift-tools-version: 6.0 +``` + +If you've completed all previous steps, you should have minimal new errors. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.4: Steps to migrate existing code to Swift 6 and Strict Concurrency Checking](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Migration Tooling for Upcoming Features + +Swift 6.2+ includes **semi-automatic migration** for upcoming features. + +### Xcode Migration + +1. Go to Build Settings → Find the upcoming feature (e.g., "Require Existential any") +2. Set to **Migrate** (temporary setting) +3. Build the project +4. Warnings appear with **Apply** buttons +5. Click Apply for each warning + +**Example warning**: +```swift +// ⚠️ Use of protocol 'Error' as a type must be written 'any Error' +func fetchData() throws -> Data // Before +func fetchData() throws -> any Data // After applying fix +``` + +### Package Migration + +Use the `swift package migrate` command: + +```bash +# Migrate all targets +swift package migrate --to-feature ExistentialAny + +# Migrate specific target +swift package migrate --target MyTarget --to-feature ExistentialAny +``` + +**Output**: +``` +> Applied 24 fix-its in 11 files (0.016s) +> Updating manifest +``` + +The tool automatically: +- Applies all fix-its +- Updates `Package.swift` to enable the feature + +**Available migrations** (as of Swift 6.2): +- `ExistentialAny` (SE-335) +- `InferIsolatedConformances` (SE-470) +- More features will add migration support over time + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.6: Migration tooling for upcoming Swift features](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +**Additional resource**: [Migration Tooling Video](https://youtu.be/FK9XFxSWZPg?si=2z_ybn1t1YCJow5k) + +--- + +## Rewriting Closures to Async/Await + +### Using Xcode Refactoring + +Three refactoring options available: + +1. **Add Async Wrapper**: Wraps existing closure-based method (recommended first step) +2. **Add Async Alternative**: Rewrites method as async, keeps original +3. **Convert Function to Async**: Replaces method entirely + +**⚠️ Known Issue**: Refactoring can be unstable in Xcode. If you get "Connection interrupted" errors: +- Clean build folder +- Clear derived data +- Restart Xcode +- Simplify complex methods (shorthand if statements can cause failures) + +### Manual Rewriting Example + +**Before** (closure-based): +```swift +func fetchImage(urlRequest: URLRequest, + completion: @escaping @Sendable (Result) -> Void) { + URLSession.shared.dataTask(with: urlRequest) { data, _, error in + do { + if let error = error { throw error } + guard let data = data, let image = UIImage(data: data) else { + throw ImageError.conversionFailed + } + completion(.success(image)) + } catch { + completion(.failure(error)) + } + }.resume() +} +``` + +**After** (async/await): +```swift +func fetchImage(urlRequest: URLRequest) async throws -> UIImage { + let (data, _) = try await URLSession.shared.data(for: urlRequest) + guard let image = UIImage(data: data) else { + throw ImageError.conversionFailed + } + return image +} +``` + +**Benefits**: +- Less code to maintain +- Easier to reason about +- No nested closures +- Automatic error propagation + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.7: Techniques for rewriting closures to async/await syntax](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Using @preconcurrency + +Suppresses `Sendable` warnings from modules you don't control. + +### When to Use + +```swift +// ⚠️ Third-party library doesn't support Swift Concurrency yet +@preconcurrency import SomeThirdPartyLibrary + +actor DataProcessor { + func process(_ data: LibraryType) { // No Sendable warning + // ... + } +} +``` + +### Risks + +- **No compile-time safety**: You're responsible for ensuring thread safety +- **Hides real issues**: Library might not be thread-safe at all +- **Technical debt**: Easy to forget to revisit later + +### Best Practices + +1. **Don't use by default**: Only add when compiler suggests it +2. **Check for updates first**: Library might have a newer version with concurrency support +3. **Document why**: Add a comment explaining why it's needed +4. **Revisit regularly**: Set reminders to check if library has been updated + +```swift +// TODO: Remove @preconcurrency when SomeLibrary adds Sendable support +// Last checked: 2026-01-07 (version 2.3.0) +@preconcurrency import SomeLibrary +``` + +The compiler will warn if `@preconcurrency` is unused: +``` +'@preconcurrency' attribute on module 'SomeModule' is unused +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.8: How and when to use @preconcurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Migrating from Combine/RxSwift + +### Observation Alternative + +Swift 6 will include **Transactional Observation** (SE-475): + +```swift +// Future API (not yet implemented) +let names = Observations { person.name } + +Task.detached { + for await name in names { + print("Name updated to: \(name)") + } +} +``` + +**Current alternatives**: +- Use `@Observable` macro for SwiftUI +- Use `AsyncStream` for custom observation +- Consider [AsyncExtensions](https://github.com/sideeffect-io/AsyncExtensions) package + +### Debouncing Example + +**Combine**: +```swift +$searchQuery + .debounce(for: .milliseconds(500), scheduler: DispatchQueue.main) + .sink { [weak self] query in + self?.performSearch(query) + } + .store(in: &cancellables) +``` + +**Swift Concurrency**: +```swift +func search(_ query: String) { + currentSearchTask?.cancel() + + currentSearchTask = Task { + do { + try await Task.sleep(for: .milliseconds(500)) + performSearch(query) + } catch { + // Search was cancelled + } + } +} +``` + +**SwiftUI Integration**: +```swift +struct SearchView: View { + @State private var searchQuery = "" + @State private var searcher = ArticleSearcher() + + var body: some View { + List(searcher.results) { result in + Text(result.title) + } + .searchable(text: $searchQuery) + .onChange(of: searchQuery) { _, newValue in + searcher.search(newValue) + } + } +} +``` + +### Mindset Shift + +**Don't think in Combine pipelines**. Many problems are simpler without FRP: + +```swift +// ❌ Looking for AsyncSequence equivalent of complex Combine pipeline +somePublisher + .debounce(for: .seconds(0.5)) + .removeDuplicates() + .flatMap { ... } + .sink { ... } + +// ✅ Rethink the problem with Swift Concurrency +Task { + var lastValue: String? + for await value in stream { + guard value != lastValue else { continue } + lastValue = value + try await Task.sleep(for: .seconds(0.5)) + await process(value) + } +} +``` + +**For complex operators**: Check [Swift Async Algorithms](https://github.com/apple/swift-async-algorithms) package. + +### ⚠️ Critical: Actor Isolation with Combine + +**Problem**: `sink` closures don't respect actor isolation at compile time. + +```swift +@MainActor +final class NotificationObserver { + private var cancellables: [AnyCancellable] = [] + + init() { + NotificationCenter.default.publisher(for: .someNotification) + .sink { [weak self] _ in + self?.handleNotification() // ⚠️ May crash if posted from background + } + .store(in: &cancellables) + } + + private func handleNotification() { + // Expects to run on main actor + } +} +``` + +**Why it crashes**: Notification observers run on the same thread as the poster. If posted from a background thread, the `@MainActor` method is called off the main actor. + +**Solutions**: + +1. **Migrate to Swift Concurrency** (recommended): +```swift +Task { [weak self] in + for await _ in NotificationCenter.default.notifications(named: .someNotification) { + await self?.handleNotification() // ✅ Compile-time safe + } +} +``` + +2. **Use Task wrapper** (temporary): +```swift +.sink { [weak self] _ in + Task { @MainActor in + self?.handleNotification() + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.9: Migrating away from Functional Reactive Programming like RxSwift or Combine](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Concurrency-Safe Notifications (iOS 26+) + +Swift 6.2 introduces **typed, thread-safe notifications**. + +### MainActorMessage + +For notifications that should be delivered on the main actor: + +```swift +// Old way +NotificationCenter.default.addObserver( + forName: UIApplication.didBecomeActiveNotification, + object: nil, + queue: .main +) { [weak self] _ in + self?.handleDidBecomeActive() // ⚠️ Concurrency warning +} + +// New way (iOS 26+) +token = NotificationCenter.default.addObserver( + of: UIApplication.self, + for: .didBecomeActive +) { [weak self] message in + self?.handleDidBecomeActive() // ✅ No warning, guaranteed main actor +} +``` + +**Key difference**: Observer closure is guaranteed to run on `@MainActor`. + +### AsyncMessage + +For notifications delivered asynchronously on arbitrary isolation: + +```swift +struct RecentBuildsChangedMessage: NotificationCenter.AsyncMessage { + typealias Subject = [RecentBuild] + let recentBuilds: Subject +} + +// Enable static member lookup +extension NotificationCenter.MessageIdentifier +where Self == NotificationCenter.BaseMessageIdentifier { + static var recentBuildsChanged: NotificationCenter.BaseMessageIdentifier { + .init() + } +} +``` + +**Posting**: +```swift +let builds = [RecentBuild(appName: "Stock Analyzer")] +let message = RecentBuildsChangedMessage(recentBuilds: builds) +NotificationCenter.default.post(message) +``` + +**Observing**: +```swift +// Old way: Unsafe casting +NotificationCenter.default.addObserver(forName: .recentBuildsChanged, object: nil, queue: nil) { notification in + guard let builds = notification.object as? [RecentBuild] else { return } + handleBuilds(builds) +} + +// New way: Strongly typed, thread-safe +token = NotificationCenter.default.addObserver( + of: [RecentBuild].self, + for: .recentBuildsChanged +) { message in + handleBuilds(message.recentBuilds) // ✅ Direct access, no casting +} +``` + +**Benefits**: +- Strongly typed (no `Any` casting) +- Compile-time thread safety +- Clear isolation guarantees + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.10: Migrating to concurrency-safe notifications](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Common Challenges + +### "It's Too Much Work" + +Break it down: +- Migrate one package at a time +- Use 30-minute daily sessions +- Create checkpoints with small PRs +- Celebrate incremental progress + +### "My Team Isn't Ready" + +Start small: +- Enable Swift 6 for new files only +- Make new types `Sendable` by default +- Share learnings in team meetings +- Pair program on tricky migrations + +### "Dependencies Aren't Ready" + +Options: +- Update to latest versions first +- Use `@preconcurrency` temporarily +- Contribute fixes to open-source dependencies +- Wrap third-party APIs in your own concurrency-safe layer + +### "I Keep Going in Circles" + +This is the "concurrency rabbit hole": +- Take breaks and revisit later +- Temporarily disable strict checking to make progress elsewhere +- Focus on one module at a time +- Don't try to fix everything at once + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 12.11: Frequently Asked Questions (FAQ) around Swift 6 Migrations](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +--- + +## Summary + +Migration to Swift 6 is a journey, not a sprint: + +1. **Start small**: Find isolated code with minimal dependencies +2. **Be incremental**: Use the three strict concurrency levels (Minimal → Targeted → Complete) +3. **Use tooling**: Leverage Xcode refactoring and `swift package migrate` +4. **Create checkpoints**: Small, focused PRs that can be merged quickly +5. **Stay positive**: The concurrency rabbit hole is real, but manageable with the right habits +6. **Think differently**: Let go of the threading mindset; trust the compiler + +The result is **compile-time thread safety**, more maintainable code, and a future-proof codebase. + +**Additional resources**: +- [Approachable Concurrency Video](https://youtu.be/y_Qc8cT-O_g?si=y4C1XQDGtyIOLW81) +- [Migration Tooling Video](https://youtu.be/FK9XFxSWZPg?si=2z_ybn1t1YCJow5k) +- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com) for in-depth migration strategies + diff --git a/.claude/skills/swift-concurrency/references/performance.md b/.claude/skills/swift-concurrency/references/performance.md new file mode 100644 index 0000000..ff07d6b --- /dev/null +++ b/.claude/skills/swift-concurrency/references/performance.md @@ -0,0 +1,574 @@ +# Performance + +Optimizing Swift Concurrency code for speed and efficiency. + +## Core Principles + +### Measurement is essential + +Can't improve what you don't measure. Establish baseline before optimizing. + +### Start simple, optimize later + +``` +Synchronous → Asynchronous → Parallel +``` + +Move right only when proven necessary. + +### Three phases of concurrency + +1. **No concurrency** - Synchronous method +2. **Suspend without parallelism** - Asynchronous method +3. **Advanced concurrency** - Parallel execution + +## Common Performance Issues + +### UI hangs + +Too much work on main thread causes interface freezes. + +### Poor parallelization + +Heavy work funneled into single task instead of parallel execution. + +### Actor contention + +Tasks waiting on busy actor, causing unnecessary suspensions. + +## Using Xcode Instruments + +### Swift Concurrency template + +Profile with CMD + I → Select "Swift Concurrency" template. + +**Instruments included**: +- **Swift Tasks**: Track running, alive, total tasks +- **Swift Actors**: Show actor execution and queue size + +### Key metrics + +``` +Tasks: +- Total count +- Running vs suspended +- Task states (Creating, Running, Suspended, Ending) + +Actors: +- Queue size +- Execution time +- Contention points + +Main Thread: +- Hangs +- Blocked time +``` + +### Task states + +- **Creating**: Task being initialized +- **Running**: Actively executing +- **Suspended**: Waiting (at await) +- **Ending**: Completing + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.1: Using Xcode Instruments to find performance bottlenecks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Identifying Issues + +### Main thread blocked + +```swift +// ❌ All work on main thread +@MainActor +func generateWallpapers() { + Task { + for _ in 0..<100 { + let image = generator.generate() // Blocks main thread + wallpapers.append(image) + } + } +} +``` + +**Instruments shows**: Long main thread hang, no parallelism. + +### Solution: Move to background + +```swift +@MainActor +func generateWallpapers() { + Task { + for _ in 0..<100 { + let image = await backgroundGenerator.generate() + wallpapers.append(image) + } + } +} + +actor BackgroundGenerator { + func generate() -> Image { + // Heavy work in background + } +} +``` + +### Actor contention + +```swift +actor Generator { + func generate() -> Image { + // Heavy work + } +} + +// ❌ Sequential through actor +for _ in 0..<100 { + let image = await generator.generate() // Queue size = 1 +} +``` + +**Instruments shows**: Actor queue never exceeds 1, no parallelism. + +### Solution: Remove unnecessary actor + +```swift +struct Generator { + @concurrent + static func generate() async -> Image { + // Heavy work, no shared state + } +} + +// ✅ Parallel execution +for i in 0..<100 { + Task(name: "Image \(i)") { + let image = await Generator.generate() + await addToCollection(image) + } +} +``` + +## Suspension Points + +### What creates suspension + +Every `await` is potential suspension point: + +```swift +let data = await fetchData() // May suspend +``` + +**Not guaranteed** - if isolation matches, may not suspend. + +### Suspension surface area + +Code between suspension points. Larger = harder to reason about: +- Actor invariants +- Performance +- Thread hops +- Reentrancy +- State consistency + +### Goal + +- Do work before crossing isolation +- Cross once +- Finish job +- Only cross again when necessary + +## Reducing Suspensions + +### 1. Use synchronous methods + +```swift +// ❌ Unnecessary async +private func scale(_ image: CGImage) async { } + +func process(_ image: CGImage) async { + let scaled = await scale(image) // Suspension point +} + +// ✅ Synchronous helper +private func scale(_ image: CGImage) { } + +func process(_ image: CGImage) async { + let scaled = scale(image) // No suspension +} +``` + +**Rule**: If method doesn't need to suspend, don't mark async. + +### 2. Prevent actor reentrancy + +```swift +// ❌ Reenters actor +actor BankAccount { + func deposit(_ amount: Int) async { + balance += amount + await logTransaction() // Leaves actor + balance += bonus // Reenters - state may have changed + } +} + +// ✅ Complete work before leaving +actor BankAccount { + func deposit(_ amount: Int) async { + balance += amount + balance += bonus + await logTransaction() // Leave after state changes + } +} +``` + +### 3. Inherit isolation + +```swift +// ❌ Switches isolation +@MainActor +func update() async { + await process() // Switches away from main actor +} + +// ✅ Inherits isolation +@MainActor +func update() async { + process() // Stays on main actor (if nonisolated(nonsending)) +} + +nonisolated(nonsending) func process() async { } +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.2: Reducing suspension points by managing isolation effectively](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### 4. Use non-suspending APIs + +```swift +// ❌ May suspend +try await Task.checkCancellation() + +// ✅ No suspension +if Task.isCancelled { + return +} +``` + +### 5. Embrace parallelism + +```swift +// ❌ Sequential +for url in urls { + let image = await download(url) + images.append(image) +} + +// ✅ Parallel +await withTaskGroup(of: Image.self) { group in + for url in urls { + group.addTask { await download(url) } + } + for await image in group { + images.append(image) + } +} +``` + +## Analyzing Suspensions in Instruments + +### View task states + +1. Select Swift Tasks instrument +2. Switch to "Task States" view +3. Look for Suspended states +4. Check suspension duration + +### Navigate to code + +1. Click task state (Running/Suspended) +2. Open Extended Detail +3. Click related method +4. Use "Open in Source Viewer" + +### Predict suspensions + +```swift +Task { + // State 1: Running + // State 2: Suspended (switch to background) + let data = await backgroundWork() + // State 3: Running (in background) + // State 4: Suspended (switch to main actor) + // State 5: Running (on main actor) + await MainActor.run { + updateUI(data) + } +} +``` + +### Optimization example + +```swift +// Before: Two suspensions +Task { + let data = await generate() // Suspension 1 + self.items.append(data) // Suspension 2 (back to main) +} + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.3: Using Xcode Instruments to detect and remove suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +// After: One suspension +Task { @concurrent in + let data = generate() // No suspension (synchronous) + await MainActor.run { + self.items.append(data) // Suspension 1 (to main) + } +} +``` + +## Choosing Execution Style + +### Decision checklist + +**Use async/parallel if**: +- [ ] Blocks main actor visibly (>16ms) +- [ ] Scales with data (N items → N cost) +- [ ] Involves I/O (network, disk) +- [ ] Benefits from combining operations +- [ ] Called frequently + +**2+ checks** → async/parallel justified. + +### Start synchronous + +```swift +// Start here +func processData(_ data: Data) -> Result { + // Fast, in-memory work +} +``` + +**Only move to async if**: +- Instruments show main thread hang +- User reports sluggishness +- Work scales with input size + +### When to use async + +```swift +func processData(_ data: Data) async -> Result { + // Use when: + // - Touches persistent storage + // - Parses large datasets + // - Network communication + // - Proven slow by profiling +} +``` + +### When to use parallel + +```swift +await withTaskGroup(of: Result.self) { group in + for item in items { + group.addTask { await process(item) } + } +} + +// Use when: +// - Multiple independent operations +// - Time-to-first-result matters + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 10.4: How to choose between serialized, asynchronous, and parallel execution](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) +// - Work scales with collection size +// - Proven beneficial by profiling +``` + +## Parallelism Costs + +### Tradeoffs + +**Benefits**: +- Faster completion (if CPU-bound) +- Better resource utilization +- Improved responsiveness + +**Costs**: +- Increased memory pressure +- CPU scheduling overhead +- System resource saturation +- Battery drain +- Thermal impact + +### When parallelism hurts + +```swift +// ❌ Over-parallelization +for i in 0..<1000 { + Task { await lightWork(i) } +} +// Creates 1000 tasks for trivial work +``` + +**Better**: Batch work or use fewer tasks. + +## UX-Driven Decisions + +### Smooth animations > raw speed + +```swift +// 80ms on main thread, but animation stutters +@MainActor +func process() { + heavyWork() // Freezes UI for 1 frame +} + +// 100ms total, but smooth UI +@MainActor +func process() async { + await backgroundWork() // UI stays responsive +} +``` + +**Perception**: Smooth feels faster than raw speed. + +### Progress indication + +```swift +@MainActor +func loadItems() async { + isLoading = true + + for i in 0..<100 { + let item = await fetchItem(i) + items.append(item) + progress = Double(i) / 100 // Incremental updates + } + + isLoading = false +} +``` + +Background work + progress = feels faster. + +## Optimization Checklist + +Before optimizing, ask: + +- [ ] Have I profiled with Instruments? +- [ ] Is main thread actually blocked? +- [ ] Can this be synchronous? +- [ ] Am I over-parallelizing? +- [ ] Is actor contention the issue? +- [ ] Are suspensions necessary? +- [ ] Does UX require background work? +- [ ] Will this scale with data? + +## Common Patterns + +### Move heavy work to background + +```swift +// Before +@MainActor +func generate() { + for _ in 0..<100 { + let item = heavyGeneration() + items.append(item) + } +} + +// After +@MainActor +func generate() async { + for _ in 0..<100 { + let item = await backgroundGenerate() + items.append(item) + } +} + +@concurrent +func backgroundGenerate() async -> Item { + // Heavy work off main thread +} +``` + +### Parallelize independent work + +```swift +// Before: Sequential +for url in urls { + let image = await download(url) + images.append(image) +} + +// After: Parallel +await withTaskGroup(of: Image.self) { group in + for url in urls { + group.addTask { await download(url) } + } + for await image in group { + images.append(image) + } +} +``` + +### Reduce actor hops + +```swift +// Before: Multiple hops +actor Store { + func process() async { + let a = await fetch1() // Hop 1 + let b = await fetch2() // Hop 2 + let c = await fetch3() // Hop 3 + combine(a, b, c) + } +} + +// After: Batch fetches +actor Store { + func process() async { + async let a = fetch1() + async let b = fetch2() + async let c = fetch3() + combine(await a, await b, await c) // One hop + } +} +``` + +## Best Practices + +1. **Profile before optimizing** - measure baseline +2. **Start synchronous** - add async only when needed +3. **Use Instruments regularly** - catch issues early +4. **Name tasks** - easier debugging in Instruments +5. **Check suspension count** - reduce unnecessary awaits +6. **Avoid premature parallelism** - has costs +7. **Consider UX** - smooth > fast +8. **Batch actor work** - reduce contention +9. **Test on real devices** - simulators lie +10. **Monitor in production** - real usage patterns differ + +## Debugging Performance + +### Instruments workflow + +1. Profile with Swift Concurrency template +2. Identify main thread hangs +3. Check task parallelism +4. Analyze actor queue sizes +5. Review suspension points +6. Navigate to problematic code +7. Apply optimizations +8. Re-profile to verify + +### Red flags in Instruments + +- Main thread blocked >16ms +- Actor queue size always 1 +- High suspension count +- Tasks created but not running +- Excessive task creation (1000+) + +## Further Learning + +For real-world optimization examples, profiling techniques, and advanced performance patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/sendable.md b/.claude/skills/swift-concurrency/references/sendable.md new file mode 100644 index 0000000..7a41979 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/sendable.md @@ -0,0 +1,578 @@ +# Sendable + +Type safety patterns for sharing data across concurrency boundaries. + +## What is Sendable? + +`Sendable` indicates a type is safe to share across isolation domains (actors, tasks, threads). The compiler verifies thread-safety at compile time. + +```swift +public protocol Sendable {} +``` + +Empty protocol, but triggers compiler verification of thread-safety. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.1: Explaining the concept of Sendable in Swift](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Isolation Domains + +Three types of isolation in Swift Concurrency: + +### 1. Nonisolated (default) + +No concurrency restrictions, but can't modify isolated state: + +```swift +func computeValue(a: Int, b: Int) -> Int { + return a + b +} +``` + +### 2. Actor-isolated + +Dedicated isolation domain with serialized access: + +```swift +actor Library { + var books: [String] = [] + + func addBook(_ title: String) { + books.append(title) + } +} + +// External access requires await +await library.addBook("Swift Concurrency") +``` + +### 3. Global actor-isolated + +Shared isolation domain across types: + +```swift +@MainActor +func updateUI() { + // Runs on main thread +} +``` + +## Data Races vs Race Conditions + +### Data Race + +Multiple threads access shared mutable state, at least one writes, without synchronization: + +```swift +// ⚠️ Data race +var counter = 0 +DispatchQueue.global().async { counter += 1 } +DispatchQueue.global().async { counter += 1 } +``` + +**Detection**: Enable Thread Sanitizer in scheme settings. + +**Prevention**: Use actors or Sendable types: + +```swift +actor Counter { + private var value = 0 + + func increment() { + value += 1 + } +} +``` + +### Race Condition + +Timing-dependent behavior leading to unpredictable results: + +```swift +let counter = Counter() + +for _ in 1...10 { + Task { await counter.increment() } +} + +// May print inconsistent values +print(await counter.getValue()) +``` + +**Key difference**: Swift Concurrency prevents data races but not race conditions. You must still ensure proper sequencing. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.2: Understanding Data Races vs. Race Conditions: Key Differences Explained](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Value Types (Structs, Enums) + +### Implicit conformance + +Non-public structs/enums with Sendable members: + +```swift +// Implicitly Sendable +struct Person { + var name: String +} +``` + +### Explicit conformance required + +Public types need explicit declaration: + +```swift +public struct Person: Sendable { + var name: String +} +``` + +**Why**: Compiler can't verify internal details of public types across modules. + +### Frozen types + +Public frozen types can be implicitly Sendable: + +```swift +@frozen +public struct Point: Sendable { + public var x: Double + public var y: Double +} +``` + +### All members must be Sendable + +```swift +public struct Person: Sendable { + var name: String + var hometown: Location // Must also be Sendable +} + +public struct Location: Sendable { + var name: String +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.3: Conforming your code to the Sendable protocol](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Copy-on-write makes mutability safe + +```swift +public struct Person: Sendable { + var name: String // Mutable but safe due to COW +} +``` + +Each mutation creates a copy, preventing concurrent access to same instance. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.4: Sendable and Value Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Reference Types (Classes) + +### Requirements for Sendable classes + +Must be: +1. `final` (no inheritance) +2. Immutable stored properties only +3. All properties Sendable +4. No superclass or `NSObject` only + +```swift +final class User: Sendable { + let name: String + let id: Int + + init(name: String, id: Int) { + self.name = name + self.id = id + } +} +``` + +### Why non-final classes can't be Sendable + +Child classes could introduce unsafe mutability: + +```swift +// Can't be Sendable +class Purchaser { + func purchase() { } +} + +// Could introduce data races +class GamePurchaser: Purchaser { + var credits: Int = 0 // Mutable! +} +``` + +### Actor isolation makes classes Sendable + +```swift +@MainActor +class ViewModel { + var data: [Item] = [] // Safe due to actor isolation +} +// Implicitly Sendable +``` + +### Composition over inheritance + +```swift +final class Purchaser: Sendable { + func purchase() { } +} + +final class GamePurchaser { + let purchaser: Purchaser = Purchaser() + // Handle credits separately +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.5: Sendable and Reference Types](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Functions and Closures (@Sendable) + +Mark functions/closures that cross isolation domains: + +```swift +actor ContactsStore { + func removeAll(_ shouldRemove: @Sendable (Contact) -> Bool) async { + contacts.removeAll { shouldRemove($0) } + } +} +``` + +### Captured values must be Sendable + +```swift +let query = "search" + +// ✅ Immutable capture +store.filter { contact in + contact.name.contains(query) +} + +var query = "search" + +// ❌ Mutable capture +store.filter { contact in + contact.name.contains(query) // Error +} +``` + +### Capture lists for mutable values + +```swift +var query = "search" + +// ✅ Capture immutable snapshot +store.filter { [query] contact in + contact.name.contains(query) +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.6: Using @Sendable with closures](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## @unchecked Sendable + +**Use as last resort.** Tells compiler to skip verification—you guarantee thread-safety. + +### When to use + +Manual locking mechanisms the compiler can't verify: + +```swift +final class Cache: @unchecked Sendable { + private let lock = NSLock() + private var items: [String: Data] = [:] + + func get(_ key: String) -> Data? { + lock.lock() + defer { lock.unlock() } + return items[key] + } + + func set(_ key: String, value: Data) { + lock.lock() + defer { lock.unlock() } + items[key] = value + } +} +``` + +### Risks + +- No compile-time safety +- Easy to introduce data races +- Must manually ensure all access uses lock + +```swift +final class Cache: @unchecked Sendable { + private let lock = NSLock() + private var items: [String: Data] = [:] + + // ⚠️ Forgot lock - data race! + var count: Int { + items.count + } +} +``` + +**Better**: Use actor instead: + +```swift +actor Cache { + private var items: [String: Data] = [:] + + var count: Int { items.count } + + func get(_ key: String) -> Data? { + items[key] + } + + func set(_ key: String, value: Data) { + items[key] = value + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.7: Using @unchecked Sendable](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Region-Based Isolation + +Compiler allows non-Sendable types in same scope: + +```swift +class Article { + var title: String + init(title: String) { self.title = title } +} + +func check() { + let article = Article(title: "Swift") + + Task { + print(article.title) // ✅ OK - same region + } +} +``` + +**Why**: No mutation after transfer, so no data race risk. + +### Breaks when accessed after transfer + +```swift +func check() { + let article = Article(title: "Swift") + + Task { + print(article.title) + } + + print(article.title) // ❌ Error - accessed after transfer +} +``` + +## The sending Keyword + +Enforces ownership transfer for non-Sendable types: + +### Parameter values + +```swift +actor Logger { + func log(article: Article) { + print(article.title) + } +} + +func printTitle(article: sending Article) async { + let logger = Logger() + await logger.log(article: article) +} + +// Usage +let article = Article(title: "Swift") +await printTitle(article: article) +// article no longer accessible here +``` + +### Return values + +```swift +@SomeActor +func createArticle(title: String) -> sending Article { + return Article(title: title) +} +``` + +Transfers ownership to caller's region. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.8: Understanding region-based isolation and the sending keyword](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Global Variables + +Must be concurrency-safe since accessible from any context. + +### Problem + +```swift +class ImageCache { + static var shared = ImageCache() // ⚠️ Not concurrency-safe +} +``` + +### Solution 1: Actor isolation + +```swift +@MainActor +class ImageCache { + static var shared = ImageCache() +} +``` + +### Solution 2: Immutable + Sendable + +```swift +final class ImageCache: Sendable { + static let shared = ImageCache() +} +``` + +### Solution 3: nonisolated(unsafe) + +**Last resort** - you guarantee safety: + +```swift +struct APIProvider: Sendable { + nonisolated(unsafe) static private(set) var shared: APIProvider! + + static func configure(apiURL: URL) { + shared = APIProvider(apiURL: apiURL) + } +} +``` + +Use `private(set)` to limit mutation points. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.9: Concurrency-safe global variables](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Custom Locks + Sendable + +### Legacy code with locks + +```swift +final class BankAccount: @unchecked Sendable { + private var balance: Int = 0 + private let lock = NSLock() + + func deposit(amount: Int) { + lock.lock() + balance += amount + lock.unlock() + } + + func getBalance() -> Int { + lock.lock() + defer { lock.unlock() } + return balance + } +} +``` + +### Migration strategy + +**New code**: Use actors + +**Existing code**: +1. If isolated and small scope → migrate to actor +2. If widely used → use `@unchecked Sendable`, file migration ticket + +```swift +// Better: Migrate to actor +actor BankAccount { + private var balance: Int = 0 + + func deposit(amount: Int) { + balance += amount + } + + func getBalance() -> Int { + balance + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 4.10: Combining Sendable with custom Locks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Decision Tree + +``` +Need to share type across isolation domains? +├─ Value type (struct/enum)? +│ ├─ Public? → Add explicit Sendable +│ └─ Internal? → Implicit Sendable (if members Sendable) +│ +├─ Reference type (class)? +│ ├─ Can be final + immutable? → Sendable +│ ├─ Needs mutation? +│ │ ├─ Can use actor? → Use actor (automatic Sendable) +│ │ ├─ Main thread only? → @MainActor +│ │ └─ Has custom lock? → @unchecked Sendable (temporary) +│ └─ Can be struct instead? → Refactor to struct +│ +└─ Function/closure? → @Sendable attribute +``` + +## Common Patterns + +### Restructure to avoid non-Sendable dependencies + +```swift +// Instead of storing non-Sendable type +public struct Person: Sendable { + var hometown: String // Just the name + + init(hometown: Location) { + self.hometown = hometown.name + } +} +``` + +### Prefer actors for mutable state + +```swift +// Instead of @unchecked Sendable with locks +actor Cache { + private var items: [String: Data] = [:] + + func get(_ key: String) -> Data? { + items[key] + } +} +``` + +### Use @MainActor for UI-bound types + +```swift +@MainActor +class ViewModel: ObservableObject { + @Published var items: [Item] = [] +} +``` + +## Best Practices + +1. **Prefer value types** - structs/enums are easier to make Sendable +2. **Use actors for mutable state** - automatic thread-safety +3. **Avoid @unchecked Sendable** - use only for proven thread-safe code +4. **Mark public types explicitly** - don't rely on implicit conformance +5. **Ensure all members Sendable** - one non-Sendable breaks the chain +6. **Use @MainActor for UI types** - simple isolation for view models +7. **Capture immutably** - use capture lists for mutable variables +8. **Test with Thread Sanitizer** - catches runtime data races +9. **File migration tickets** - track @unchecked Sendable usage + +## Further Learning + +For migration strategies, real-world examples, and actor patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/tasks.md b/.claude/skills/swift-concurrency/references/tasks.md new file mode 100644 index 0000000..56edcc9 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/tasks.md @@ -0,0 +1,604 @@ +# Tasks + +Core patterns for creating, managing, and controlling concurrent work in Swift. + +## What is a Task? + +Tasks bridge synchronous and asynchronous contexts. They start executing immediately upon creation—no `resume()` needed. + +```swift +func synchronousMethod() { + Task { + await someAsyncMethod() + } +} +``` + +## Task References + +Storing a reference is optional but enables cancellation and result waiting: + +```swift +final class ImageLoader { + var loadTask: Task? + + func load() { + loadTask = Task { + try await fetchImage() + } + } + + deinit { + loadTask?.cancel() + } +} +``` + +Tasks run regardless of whether you keep a reference. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.1: Introduction to tasks in Swift Concurrency](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Cancellation + +### Checking for cancellation + +Tasks must manually check for cancellation: + +```swift +// Throws CancellationError if canceled +try Task.checkCancellation() + +// Boolean check for custom handling +guard !Task.isCancelled else { + return fallbackValue +} +``` + +### Where to check + +Add checks at natural breakpoints: + +```swift +let task = Task { + // Before expensive work + try Task.checkCancellation() + + let data = try await URLSession.shared.data(from: url) + + // After network, before processing + try Task.checkCancellation() + + return processData(data) +} +``` + +### Child task cancellation + +Canceling a parent automatically notifies all children: + +```swift +let parent = Task { + async let child1 = work(1) + async let child2 = work(2) + let results = try await [child1, child2] +} + +parent.cancel() // Both children notified +``` + +Children must still check `Task.isCancelled` to stop work. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.2: Task cancellation](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Error Handling + +Task error types are inferred from the operation: + +```swift +// Can throw +let throwingTask: Task = Task { + throw URLError(.badURL) +} + +// Cannot throw +let nonThrowingTask: Task = Task { + "Success" +} +``` + +### Awaiting results + +```swift +do { + let result = try await task.value +} catch { + // Handle error +} +``` + +### Handling errors internally + +```swift +let safeTask: Task = Task { + do { + return try await riskyOperation() + } catch { + return "Fallback value" + } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.3: Error handling in Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## SwiftUI Integration + +### The .task modifier + +Automatically manages task lifetime with view lifecycle: + +```swift +struct ContentView: View { + @State private var data: Data? + + var body: some View { + Text(data?.description ?? "Loading...") + .task { + data = try? await fetchData() + } + } +} +``` + +Task cancels automatically when view disappears. + +### Reacting to value changes + +```swift +.task(id: searchQuery) { + await performSearch(searchQuery) +} +``` + +When `searchQuery` changes: +1. Previous task cancels +2. New task starts with updated value + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.12: Running tasks in SwiftUI](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Priority configuration + +```swift +// High priority (default for SwiftUI) +.task(priority: .userInitiated) { + await fetchUserData() +} + +// Lower priority for background work +.task(priority: .low) { + await trackAnalytics() +} +``` + +## Task Groups + +Dynamic parallel task execution with compile-time unknown task count. + +### Basic usage + +```swift +await withTaskGroup(of: UIImage.self) { group in + for url in photoURLs { + group.addTask { + await downloadPhoto(url: url) + } + } +} +``` + +### Collecting results + +```swift +let images = await withTaskGroup(of: UIImage.self) { group in + for url in photoURLs { + group.addTask { await downloadPhoto(url: url) } + } + + return await group.reduce(into: []) { $0.append($1) } +} +``` + +### Error handling + +```swift +let images = try await withThrowingTaskGroup(of: UIImage.self) { group in + for url in photoURLs { + group.addTask { try await downloadPhoto(url: url) } + } + + // Iterate to propagate errors + var results: [UIImage] = [] + for try await image in group { + results.append(image) + } + return results +} +``` + +**Critical**: Errors in child tasks don't automatically fail the group. Use iteration (`for try await`, `next()`, `reduce()`) to propagate errors. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.5: Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Early termination on error + +```swift +try await withThrowingTaskGroup(of: Data.self) { group in + for id in ids { + group.addTask { try await fetch(id) } + } + + // First error cancels remaining tasks + while let data = try await group.next() { + process(data) + } +} +``` + +### Cancellation + +```swift +await withTaskGroup(of: Result.self) { group in + for item in items { + group.addTask { await process(item) } + } + + // Cancel all remaining tasks + group.cancelAll() +} +``` + +Or prevent adding to canceled group: + +```swift +let didAdd = group.addTaskUnlessCancelled { + await work() +} +``` + +## Discarding Task Groups + +For fire-and-forget operations where results don't matter: + +```swift +await withDiscardingTaskGroup { group in + group.addTask { await logEvent("user_login") } + group.addTask { await preloadCache() } + group.addTask { await syncAnalytics() } +} +``` + +### Benefits + +- More memory efficient (doesn't store results) +- No `next()` calls needed +- Automatically waits for completion +- Ideal for side effects + +### Error handling + +```swift +try await withThrowingDiscardingTaskGroup { group in + group.addTask { try await uploadLog() } + group.addTask { try await syncSettings() } +} +// First error cancels group and throws +``` + +### Real-world pattern: Multiple notifications + +```swift +extension NotificationCenter { + func notifications(named names: [Notification.Name]) -> AsyncStream<()> { + AsyncStream { continuation in + let task = Task { + await withDiscardingTaskGroup { group in + for name in names { + group.addTask { + for await _ in self.notifications(named: name) { + continuation.yield(()) + } + } + } + } + continuation.finish() + } + + continuation.onTermination = { _ in task.cancel() } + } + } +} + +// Usage +for await _ in NotificationCenter.default.notifications( + named: [.userDidLogin, UIApplication.didBecomeActiveNotification] +) { + refreshData() +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.6: Discarding Task Groups](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Structured vs Unstructured Tasks + +### Structured (preferred) + +Bound to parent, inherit context, automatic cancellation: + +```swift +// async let +async let data1 = fetch(1) +async let data2 = fetch(2) +let results = await [data1, data2] + +// Task groups +await withTaskGroup(of: Data.self) { group in + group.addTask { await fetch(1) } + group.addTask { await fetch(2) } +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.7: The difference between structured and unstructured tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Unstructured (use sparingly) + +Independent lifecycle, manual cancellation: + +```swift +// Regular task (unstructured but inherits priority) +let task = Task { + await doWork() +} + +// Detached task (completely independent) +Task.detached(priority: .background) { + await cleanup() +} +``` + +## Detached Tasks + +**Use as last resort.** They don't inherit: +- Priority +- Task-local values +- Cancellation state + +```swift +Task.detached(priority: .background) { + await DirectoryCleaner.cleanup() +} +``` + +### When to use + +- Independent background work +- No connection to parent needed +- Acceptable to complete after parent cancels +- No `self` references needed + +**Prefer**: Task groups or `async let` for most parallel work. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.4: Detached Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Task Priorities + +### Available priorities + +```swift +.high // Immediate user feedback +.userInitiated // User-triggered work (same as .high) +.medium // Default for detached tasks +.utility // Longer-running, non-urgent +.low // Similar to .background +.background // Lowest priority +``` + +### Setting priority + +```swift +Task(priority: .background) { + await prefetchData() +} +``` + +### Priority inheritance + +Structured tasks inherit parent priority: + +```swift +Task(priority: .high) { + async let result = work() // Also .high + await result +} +``` + +Detached tasks don't inherit: + +```swift +Task(priority: .high) { + Task.detached { + // Runs at .medium (default) + } +} +``` + +### Priority escalation + +System automatically elevates priority to prevent priority inversion: +- Actor waiting on lower-priority task +- High-priority task awaiting `.value` of lower-priority task + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.8: Managing Task priorities](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Task.sleep() vs Task.yield() + +### Task.sleep() + +Suspends for fixed duration, non-blocking: + +```swift +try await Task.sleep(for: .seconds(5)) +``` + +**Use for:** +- Debouncing user input +- Polling intervals +- Rate limiting +- Artificial delays + +**Respects cancellation** (throws `CancellationError`) + +### Task.yield() + +Temporarily suspends to allow other tasks to run: + +```swift +await Task.yield() +``` + +**Use for:** +- Testing async code +- Allowing cooperative scheduling + +**Note**: If current task is highest priority, may resume immediately. + +### Practical: Debounced search + +```swift +func search(_ query: String) async { + guard !query.isEmpty else { + searchResults = allResults + return + } + + do { + try await Task.sleep(for: .milliseconds(500)) + searchResults = allResults.filter { $0.contains(query) } + } catch { + // Canceled (user kept typing) + } +} + +// In SwiftUI +.task(id: searchQuery) { + await searcher.search(searchQuery) +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.10: Task.yield() vs. Task.sleep()](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## async let vs TaskGroup + +| Feature | async let | TaskGroup | +|---------|-----------|-----------| +| Task count | Fixed at compile-time | Dynamic at runtime | +| Syntax | Lightweight | More verbose | +| Cancellation | Automatic on scope exit | Manual via `cancelAll()` | +| Use when | 2-5 known parallel tasks | Loop-based parallel work | + +```swift +// async let: Known task count +async let user = fetchUser() +async let settings = fetchSettings() +let profile = Profile(user: await user, settings: await settings) + +// TaskGroup: Dynamic task count +await withTaskGroup(of: Image.self) { group in + for url in urls { + group.addTask { await download(url) } + } +} +``` + +## Advanced: Task Timeout Pattern + +Create timeout wrapper using task groups: + +```swift +func withTimeout( + _ duration: Duration, + operation: @Sendable @escaping () async throws -> T +) async throws -> T { + try await withThrowingTaskGroup(of: T.self) { group in + group.addTask { try await operation() } + + group.addTask { + try await Task.sleep(for: duration) + throw TimeoutError() + } + + guard let result = try await group.next() else { + throw TimeoutError() + } + + group.cancelAll() + return result + } +} + +// Usage +let data = try await withTimeout(.seconds(5)) { + try await slowNetworkRequest() +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 3.14: Creating a Task timeout handler using a Task Group (advanced)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Common Patterns + +### Sequential with early exit + +```swift +let user = try await fetchUser() +guard user.isActive else { return } + +let posts = try await fetchPosts(userId: user.id) +``` + +### Parallel independent work + +```swift +async let user = fetchUser() +async let settings = fetchSettings() +async let notifications = fetchNotifications() + +let data = try await (user, settings, notifications) +``` + +### Mixed: Sequential then parallel + +```swift +let user = try await fetchUser() + +async let posts = fetchPosts(userId: user.id) +async let followers = fetchFollowers(userId: user.id) + +let profile = Profile( + user: user, + posts: try await posts, + followers: try await followers +) +``` + +## Best Practices + +1. **Check cancellation regularly** in long-running tasks +2. **Use structured concurrency** (avoid detached tasks) +3. **Leverage SwiftUI's `.task` modifier** for view-bound work +4. **Choose the right tool**: `async let` for fixed, TaskGroup for dynamic +5. **Handle errors explicitly** in throwing task groups +6. **Set priority only when needed** (inherit by default) +7. **Don't mutate task groups** from outside their creation context + +## Further Learning + +For hands-on examples, advanced patterns, and migration strategies, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/.claude/skills/swift-concurrency/references/testing.md b/.claude/skills/swift-concurrency/references/testing.md new file mode 100644 index 0000000..464044e --- /dev/null +++ b/.claude/skills/swift-concurrency/references/testing.md @@ -0,0 +1,565 @@ +# Testing Concurrent Code + +Best practices for testing Swift Concurrency with Swift Testing (recommended) and XCTest. + +## Recommendation: Use Swift Testing + +**Swift Testing is strongly recommended** for new projects and tests. It provides: +- Modern Swift syntax with macros +- Better concurrency support +- Cleaner test structure +- More flexible test organization + +XCTest patterns are included for legacy codebases. + +## Swift Testing Basics + +### Simple async test + +```swift +@Test +@MainActor +func emptyQuery() async { + let searcher = ArticleSearcher() + await searcher.search("") + #expect(searcher.results == ArticleSearcher.allArticles) +} +``` + +**Key differences from XCTest**: +- `@Test` macro instead of `XCTestCase` +- `#expect` instead of `XCTAssert` +- Structs preferred over classes +- No `test` prefix required + +### Testing with actors + +```swift +@Test +@MainActor +func searchReturnsResults() async { + let searcher = ArticleSearcher() + await searcher.search("swift") + #expect(!searcher.results.isEmpty) +} +``` + +Mark test with actor if system under test requires it. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.2: Testing concurrent code using Swift Testing](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Awaiting Async Callbacks + +### Using continuations + +When testing unstructured tasks: + +```swift +@Test +@MainActor +func searchTaskCompletes() async { + let searcher = ArticleSearcher() + + await withCheckedContinuation { continuation in + _ = withObservationTracking { + searcher.results + } onChange: { + continuation.resume() + } + + searcher.startSearchTask("swift") + } + + #expect(searcher.results.count > 0) +} +``` + +**Use when**: Testing code that spawns unstructured tasks. + +### Using confirmations + +For structured async code: + +```swift +@Test +@MainActor +func searchTriggersObservation() async { + let searcher = ArticleSearcher() + + await confirmation { confirm in + _ = withObservationTracking { + searcher.results + } onChange: { + confirm() + } + + // Must await here for confirmation to work + await searcher.search("swift") + } + + #expect(!searcher.results.isEmpty) +} +``` + +**Critical**: Must `await` async work for confirmation to validate. + +## Setup and Teardown + +### Using init/deinit + +```swift +@MainActor +final class DatabaseTests { + let database: Database + + init() async throws { + database = Database() + await database.prepare() + } + + deinit { + // Synchronous cleanup only + } + + @Test + func insertsData() async throws { + try await database.insert(item) + #expect(await database.count() == 1) + } +} +``` + +**Limitation**: `deinit` cannot call async methods. + +### Test Scoping Traits + +For async teardown: + +```swift +@MainActor +struct DatabaseTrait: SuiteTrait, TestTrait, TestScoping { + func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: () async throws -> Void + ) async throws { + let database = Database() + + try await Environment.$database.withValue(database) { + await database.prepare() + try await function() + await database.cleanup() // Async teardown + } + } +} + +// Environment for task-local storage +@MainActor +struct Environment { + @TaskLocal static var database = Database() +} + +// Apply to suite +@Suite(DatabaseTrait()) +@MainActor +final class DatabaseTests { + @Test + func insertsData() async throws { + try await Environment.database.insert(item) + } +} + +// Or apply to individual test +@Test(DatabaseTrait()) +func specificTest() async throws { + // Test code +} +``` + +**Use when**: Need async cleanup after each test. + +## Handling Flaky Tests + +### Problem: Race conditions + +```swift +@Test +@MainActor +func isLoadingState() async throws { + let fetcher = ImageFetcher() + + let task = Task { try await fetcher.fetch(url) } + + // ❌ Flaky - may pass or fail + #expect(fetcher.isLoading == true) + + try await task.value + #expect(fetcher.isLoading == false) +} +``` + +**Issue**: Task may complete before we check `isLoading`. + +### Solution: Swift Concurrency Extras + +```swift +import ConcurrencyExtras + +@Test +@MainActor +func isLoadingState() async throws { + try await withMainSerialExecutor { + let fetcher = ImageFetcher { url in + await Task.yield() // Allow test to check state + return Data() + } + + let task = Task { try await fetcher.fetch(url) } + + await Task.yield() // Switch to task + + #expect(fetcher.isLoading == true) // ✅ Reliable + + try await task.value + #expect(fetcher.isLoading == false) + } +} +``` + +**Add package**: `https://github.com/pointfreeco/swift-concurrency-extras.git` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.3: Using Swift Concurrency Extras by Point-Free](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Serial execution required + +```swift +@Suite(.serialized) +@MainActor +final class ImageFetcherTests { + // Tests run serially when using withMainSerialExecutor +} +``` + +**Critical**: Main serial executor doesn't work with parallel test execution. + +## XCTest Patterns (Legacy) + +### Basic async test + +```swift +final class ArticleSearcherTests: XCTestCase { + @MainActor + func testEmptyQuery() async { + let searcher = ArticleSearcher() + await searcher.search("") + XCTAssertEqual(searcher.results, ArticleSearcher.allArticles) + } +} +``` + +### Using expectations + +```swift +@MainActor +func testSearchTask() async { + let searcher = ArticleSearcher() + let expectation = expectation(description: "Search complete") + + _ = withObservationTracking { + searcher.results + } onChange: { + expectation.fulfill() + } + + searcher.startSearchTask("swift") + + // Use fulfillment, not wait + await fulfillment(of: [expectation], timeout: 10) + + XCTAssertEqual(searcher.results.count, 1) +} +``` + +**Critical**: Use `await fulfillment(of:)`, not `wait(for:)` to avoid deadlocks. + +### Setup and teardown + +```swift +final class DatabaseTests: XCTestCase { + override func setUp() async throws { + // Async setup + } + + override func tearDown() async throws { + // Async teardown + } +} +``` + +Mark as `async throws` to call async methods. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 11.1: Testing concurrent code using XCTest](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +### Main serial executor for all tests + +```swift +final class MyTests: XCTestCase { + override func invokeTest() { + withMainSerialExecutor { + super.invokeTest() + } + } +} +``` + +## Common Patterns + +### Testing @MainActor code + +```swift +@Test +@MainActor +func viewModelUpdates() async { + let viewModel = ViewModel() + await viewModel.loadData() + #expect(viewModel.items.count > 0) +} +``` + +### Testing actors + +```swift +@Test +func actorIsolation() async { + let store = DataStore() + await store.insert(item) + let count = await store.count() + #expect(count == 1) +} +``` + +### Testing cancellation + +```swift +@Test +func cancellationStopsWork() async throws { + let processor = DataProcessor() + + let task = Task { + try await processor.processLargeDataset() + } + + task.cancel() + + do { + try await task.value + Issue.record("Should have thrown cancellation error") + } catch is CancellationError { + // Expected + } +} +``` + +### Testing with delays + +```swift +@Test +func debouncedSearch() async throws { + try await withMainSerialExecutor { + let searcher = DebouncedSearcher() + + searcher.search("a") + await Task.yield() + + searcher.search("ab") + await Task.yield() + + searcher.search("abc") + + // Wait for debounce + try await Task.sleep(for: .milliseconds(600)) + + #expect(searcher.searchCount == 1) // Only last search executed + } +} +``` + +### Testing task groups + +```swift +@Test +func taskGroupProcessesAll() async throws { + let processor = BatchProcessor() + + let results = await withTaskGroup(of: Int.self) { group in + for i in 1...5 { + group.addTask { await processor.process(i) } + } + + var collected: [Int] = [] + for await result in group { + collected.append(result) + } + return collected + } + + #expect(results.count == 5) +} +``` + +## Testing Memory Management + +### Verify deallocation + +```swift +@Test +func viewModelDeallocates() async { + var viewModel: ViewModel? = ViewModel() + weak var weakViewModel = viewModel + + viewModel?.startWork() + viewModel = nil + + try? await Task.sleep(for: .milliseconds(100)) + + #expect(weakViewModel == nil) +} +``` + +### Detect retain cycles + +```swift +@Test +func noRetainCycle() async { + var manager: Manager? = Manager() + weak var weakManager = manager + + manager?.startLongRunningTask() + manager = nil + + #expect(weakManager == nil) +} +``` + +## Best Practices + +1. **Use Swift Testing for new code** - modern, better concurrency support +2. **Mark tests with correct isolation** - @MainActor when needed +3. **Use confirmations over continuations** - when structured concurrency allows +4. **Serialize tests with main serial executor** - avoid flaky tests +5. **Test cancellation explicitly** - ensure proper cleanup +6. **Verify deallocation** - catch retain cycles early +7. **Use Task.yield() strategically** - control execution in tests +8. **Avoid sleep in tests** - use continuations/confirmations instead +9. **Test actor isolation** - verify thread safety +10. **Keep tests deterministic** - avoid timing dependencies + +## Migration from XCTest + +### XCTest → Swift Testing + +```swift +// XCTest +final class MyTests: XCTestCase { + func testExample() async { + XCTAssertEqual(value, expected) + } +} + +// Swift Testing +@Suite +struct MyTests { + @Test + func example() async { + #expect(value == expected) + } +} +``` + +### Expectations → Confirmations + +```swift +// XCTest +let expectation = expectation(description: "Done") +doWork { expectation.fulfill() } +await fulfillment(of: [expectation]) + +// Swift Testing +await confirmation { confirm in + await doWork { confirm() } +} +``` + +### Setup/Teardown → Traits + +```swift +// XCTest +override func setUp() async throws { + await prepare() +} + +// Swift Testing +struct SetupTrait: TestTrait, TestScoping { + func provideScope( + for test: Test, + testCase: Test.Case?, + performing function: () async throws -> Void + ) async throws { + await prepare() + try await function() + } +} +``` + +## Troubleshooting + +### Test hangs + +**Cause**: Waiting for expectation that never fulfills. + +**Solution**: Add timeout, verify observation tracking. + +### Flaky test + +**Cause**: Race condition in unstructured task. + +**Solution**: Use main serial executor + Task.yield(). + +### Deadlock + +**Cause**: Using `wait(for:)` in async context. + +**Solution**: Use `await fulfillment(of:)` instead. + +### Confirmation fails + +**Cause**: Not awaiting async work in confirmation block. + +**Solution**: Add `await` before async calls. + +### Actor isolation error + +**Cause**: Test not marked with required actor. + +**Solution**: Add `@MainActor` or appropriate actor to test. + +## Testing Checklist + +- [ ] Tests marked with correct isolation +- [ ] Using Swift Testing (recommended) +- [ ] Async methods properly awaited +- [ ] Cancellation tested +- [ ] Memory leaks checked +- [ ] Race conditions handled +- [ ] Timeouts appropriate +- [ ] Flaky tests fixed with serial executor +- [ ] Actor isolation verified +- [ ] Cleanup in traits (not deinit) + +## Further Learning + +For advanced testing patterns, real-world examples, and migration strategies: +- [Swift Testing Documentation](https://developer.apple.com/documentation/testing) +- [Swift Concurrency Extras](https://github.com/pointfreeco/swift-concurrency-extras) +- [Swift Concurrency Course](https://www.swiftconcurrencycourse.com) + diff --git a/.claude/skills/swift-concurrency/references/threading.md b/.claude/skills/swift-concurrency/references/threading.md new file mode 100644 index 0000000..8f26119 --- /dev/null +++ b/.claude/skills/swift-concurrency/references/threading.md @@ -0,0 +1,452 @@ +# Threading + +Understanding how Swift Concurrency manages threads and execution contexts. + +## Core Concepts + +### What is a Thread? + +System-level resource that runs instructions. High overhead for creation and switching. Swift Concurrency abstracts thread management away. + +### Tasks vs Threads + +**Tasks** are units of async work, not tied to specific threads. Swift dynamically schedules tasks on available threads from a cooperative pool. + +**Key insight**: No direct relationship between one task and one thread. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.1: How Threads relate to Tasks](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +**Important (Swift 6+)**: Avoid using `Thread.current` inside async contexts. In Swift 6 language mode, `Thread.current` is unavailable from asynchronous contexts and will fail to compile. Prefer reasoning in terms of isolation domains; use Instruments and the debugger to observe execution when needed. + +## Cooperative Thread Pool + +Swift creates only as many threads as CPU cores. Tasks share these threads efficiently. + +### How it works + +1. **Limited threads**: Number matches CPU cores +2. **Task scheduling**: Tasks scheduled onto available threads +3. **Suspension**: At `await`, task suspends, thread freed for other work +4. **Resumption**: Task resumes on any available thread (not necessarily the same one) + +```swift +func example() async { + print("Started on: \(Thread.current)") + + try await Task.sleep(for: .seconds(1)) + + print("Resumed on: \(Thread.current)") // Likely different thread +} +``` + +### Benefits over GCD + +**Prevents thread explosion**: +- No excessive thread creation +- No high memory overhead from idle threads +- No excessive context switching +- No priority inversion + +**Better performance**: +- Fewer threads = less context switching +- Continuations instead of blocking +- CPU cores stay busy efficiently + +## Threading Mindset → Isolation Mindset + +### Old way (GCD) + +```swift +// Thinking about threads +DispatchQueue.main.async { + // Update UI on main thread +} + +DispatchQueue.global(qos: .background).async { + // Heavy work on background thread +} +``` + +### New way (Swift Concurrency) + +```swift +// Thinking about isolation domains +@MainActor +func updateUI() { + // Runs on main actor (usually main thread) +} + +func heavyWork() async { + // Runs on any available thread in pool +} +``` + +### Think in isolation domains + +**Don't ask**: "What thread should this run on?" + +**Ask**: "What isolation domain should own this work?" + +- `@MainActor` for UI updates +- Custom actors for specific state +- Nonisolated for general async work + +### Provide hints, not commands + +```swift +Task(priority: .userInitiated) { + await doWork() +} +``` + +You're describing the nature of work, not assigning threads. Swift optimizes execution. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.2: Getting rid of the "Threading Mindset"](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Suspension Points + +### What is a suspension point? + +Moment where task **may** pause to allow other work. Marked by `await`. + +```swift +let data = await fetchData() // Potential suspension +``` + +**Critical**: `await` marks *possible* suspension, not guaranteed. If operation completes synchronously, no suspension occurs. + +### Why suspension points matter + +1. **Code may pause unexpectedly** - resumes later, possibly different thread +2. **State can change** - mutable state may be modified during suspension +3. **Actor reentrancy** - other tasks can access actor during suspension + +### Actor reentrancy example + +```swift +actor BankAccount { + private var balance: Int = 0 + + func deposit(amount: Int) async { + balance += amount + print("Balance: \(balance)") + + await logTransaction(amount) // ⚠️ Suspension point + + balance += 10 // Bonus + print("After bonus: \(balance)") + } + + func logTransaction(_ amount: Int) async { + try? await Task.sleep(for: .seconds(1)) + } +} + +// Two concurrent deposits +async let _ = account.deposit(amount: 100) +async let _ = account.deposit(amount: 100) + +// Unexpected: 100 → 200 → 210 → 220 +// Expected: 100 → 110 → 210 → 220 +``` + +**Why**: During `logTransaction`, second deposit runs, modifying balance before first completes. + +### Avoiding reentrancy bugs + +**Complete actor work before suspending**: + +```swift +func deposit(amount: Int) async { + balance += amount + balance += 10 // Bonus applied first + print("Final balance: \(balance)") + + await logTransaction(amount) // Suspend after state changes +} +``` + +**Rule**: Don't mutate actor state after suspension points. + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.3: Understanding Task suspension points](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +## Thread Execution Patterns + +### Default: Background threads + +Tasks run on cooperative thread pool (background threads): + +```swift +Task { + print(Thread.current) // Background thread +} +``` + +### Main thread execution + +Use `@MainActor` for main thread: + +```swift +@MainActor +func updateUI() { + Task { + print(Thread.current) // Main thread + } +} +``` + +### Inheritance example + +```swift +@MainActor +func updateUI() { + print("Main thread: \(Thread.current)") + + await backgroundTask() // Switches to background + + print("Back on main: \(Thread.current)") // Returns to main +} + +func backgroundTask() async { + print("Background: \(Thread.current)") +} +``` + +## Swift 6.2 Changes + +### Nonisolated async functions (SE-461) + +**Old behavior**: Nonisolated async functions always switch to background. + +**New behavior**: Inherit caller's isolation by default. + +```swift +class NotSendable { + func performAsync() async { + print(Thread.current) + } +} + +@MainActor +func caller() async { + let obj = NotSendable() + await obj.performAsync() + // Old: Background thread + // New: Main thread (inherits @MainActor) +} +``` + +### Enabling new behavior + +In Xcode 16+: + +```swift +// Build setting or swift-settings +.enableUpcomingFeature("NonisolatedNonsendingByDefault") +``` + +### Opting out with @concurrent + +Force function to switch away from caller's isolation: + +```swift +@concurrent +func performAsync() async { + print(Thread.current) // Always background +} +``` + +### nonisolated(nonsending) + +Prevent sending non-Sendable values across isolation: + +```swift +nonisolated(nonsending) func storeTouch(...) async { + // Runs on caller's isolation, no value sending +} +``` + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.4: Dispatching to different threads using nonisolated(nonsending) and @concurrent (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) + +**Use when**: Method doesn't need to switch isolation, avoiding Sendable requirements. + +## Default Isolation Domain (SE-466) + +### Configuring default isolation + +**Build setting** (Xcode 16+): +- Default Actor Isolation: `MainActor` or `None` + +**Swift Package**: + +```swift +.target( + name: "MyTarget", + swiftSettings: [ + .defaultIsolation(MainActor.self) + ] +) +``` + +### Why change default? + +Most app code runs on main thread. Setting `@MainActor` as default: +- Reduces false warnings +- Avoids "concurrency rabbit hole" +- Makes migration easier + +### Inference with @MainActor default + +```swift +// With @MainActor as default: + +func f() {} // Inferred: @MainActor + +class C { + init() {} // Inferred: @MainActor + static var value = 10 // Inferred: @MainActor +} + +@MyActor +struct S { + func f() {} // Inferred: @MyActor (explicit override) +} + +> **Course Deep Dive**: This topic is covered in detail in [Lesson 7.5: Controlling the default isolation domain (Updated for Swift 6.2)](https://www.swiftconcurrencycourse.com?utm_source=github&utm_medium=agent-skill&utm_campaign=lesson-reference) +``` + +### Per-module setting + +Must opt in for each module/package. Not global across dependencies. + +### Backward compatibility + +Opt-in only. Default remains `nonisolated` if not specified. + +## Debugging Thread Execution + +### Print current thread + +**⚠️ Important**: `Thread.current` is unavailable in Swift 6 language mode from async contexts. The compiler error states: "Class property 'current' is unavailable from asynchronous contexts; Thread.current cannot be used from async contexts." + +**Workaround** (Swift 6+ mode only): + +```swift +extension Thread { + public static var currentThread: Thread { + Thread.current + } +} + +print("Thread: \(Thread.currentThread)") +``` + +### Debug navigator + +1. Set breakpoint in task +2. Debug → Pause +3. Check Debug Navigator for thread info + +### Verify main thread + +```swift +assert(Thread.isMainThread) +``` + +## Common Misconceptions + +### ❌ Each Task runs on new thread + +**Wrong**. Tasks share limited thread pool, reuse threads. + +### ❌ await blocks the thread + +**Wrong**. `await` suspends task without blocking thread. Other tasks can use the thread. + +### ❌ Task execution order is guaranteed + +**Wrong**. Tasks execute based on system scheduling. Use `await` to enforce order. + +### ❌ Same task = same thread + +**Wrong**. Task can resume on different thread after suspension. + +## Why Sendable Matters + +Since tasks move between threads unpredictably: + +```swift +func example() async { + print("Thread 1: \(Thread.current)") + + await someWork() + + print("Thread 2: \(Thread.current)") // Different thread +} +``` + +Values crossing suspension points may cross threads. **Sendable** ensures safety. + +## Best Practices + +1. **Stop thinking about threads** - think isolation domains +2. **Trust the system** - Swift optimizes thread usage +3. **Use @MainActor for UI** - clear, explicit main thread execution +4. **Minimize suspension points in actors** - avoid reentrancy bugs +5. **Complete state changes before suspending** - prevent inconsistent state +6. **Use priorities as hints** - not guarantees +7. **Make types Sendable** - safe across thread boundaries +8. **Enable Swift 6.2 features** - easier migration, better defaults +9. **Set default isolation for apps** - reduce false warnings +10. **Don't force thread switching** - let Swift optimize + +## Migration Strategy + +### For new projects (Xcode 16+) + +1. Set default isolation to `@MainActor` +2. Enable `NonisolatedNonsendingByDefault` +3. Use `@concurrent` for explicit background work + +### For existing projects + +1. Gradually enable Swift 6 language mode +2. Consider default isolation change +3. Use `@concurrent` to maintain old behavior where needed +4. Migrate module by module + +## Decision Tree + +``` +Need to control execution? +├─ UI updates? → @MainActor +├─ Specific state isolation? → Custom actor +├─ Background work? → Regular async (trust Swift) +└─ Need to force background? → @concurrent (Swift 6.2+) + +Seeing Sendable warnings? +├─ Can make type Sendable? → Add conformance +├─ Same isolation OK? → nonisolated(nonsending) +└─ Need different isolation? → Make Sendable or refactor +``` + +## Performance Insights + +### Why fewer threads = better performance + +- **Less context switching**: CPU spends more time on actual work +- **Better cache utilization**: Threads stay on same cores longer +- **No thread explosion**: Predictable resource usage +- **Forward progress**: Threads never block, always productive + +### Cooperative pool advantages + +- Matches hardware (one thread per core) +- Prevents oversubscription +- Efficient task scheduling +- Automatic load balancing + +## Further Learning + +For migration strategies, real-world examples, and advanced threading patterns, see [Swift Concurrency Course](https://www.swiftconcurrencycourse.com). + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..22d04ab --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,33 @@ +# AGENTS + + + +## Available Skills + + + +When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge. + +How to use skills: +- Invoke: Bash("openskills read ") +- The skill content will load with detailed instructions on how to complete the task +- Base directory provided in output for resolving bundled resources (references/, scripts/, assets/) + +Usage notes: +- Only use skills listed in below +- Do not invoke a skill that is already loaded in your context +- Each skill invocation is stateless + + + + + +swift-concurrency +'Expert guidance on Swift Concurrency best practices, patterns, and implementation. Use when developers mention: (1) Swift Concurrency, async/await, actors, or tasks, (2) "use Swift Concurrency" or "modern concurrency patterns", (3) migrating to Swift 6, (4) data races or thread safety issues, (5) refactoring closures to async/await, (6) @MainActor, Sendable, or actor isolation, (7) concurrent code architecture or performance optimization, (8) concurrency-related linter warnings (SwiftLint or similar; e.g. async_without_await, Sendable/actor isolation/MainActor lint).' +project + + + + + + diff --git a/Sources/SwiftFindRefs/CompositionRoot.swift b/Sources/SwiftFindRefs/CompositionRoot.swift index f32c333..ed7525c 100644 --- a/Sources/SwiftFindRefs/CompositionRoot.swift +++ b/Sources/SwiftFindRefs/CompositionRoot.swift @@ -10,7 +10,7 @@ struct CompositionRoot { let fileSystem: FileSystemProvider let derivedDataLocator: DerivedDataLocatorProtocol - func run() throws { + func run() async throws { let derivedDataPaths = try derivedDataLocator.locateDerivedData( projectName: projectName, derivedDataPath: derivedDataPath @@ -21,7 +21,7 @@ struct CompositionRoot { indexStorePath: derivedDataPaths.indexStoreDBURL.deletingLastPathComponent().path ) print("🔍 Searching for references to symbol '\(symbolName)' of type '\(symbolType ?? "any")'") - let references = try indexStoreFinder.fileReferences( + let references = try await indexStoreFinder.fileReferences( of: symbolName, symbolType: symbolType ) diff --git a/Sources/SwiftFindRefs/IndexStore/IndexStoreFinder.swift b/Sources/SwiftFindRefs/IndexStore/IndexStoreFinder.swift index e08a101..23c3c78 100644 --- a/Sources/SwiftFindRefs/IndexStore/IndexStoreFinder.swift +++ b/Sources/SwiftFindRefs/IndexStore/IndexStoreFinder.swift @@ -4,39 +4,46 @@ import Foundation struct IndexStoreFinder { let indexStorePath: String - func fileReferences(of symbolName: String, symbolType: String?) throws -> [String] { + func fileReferences(of symbolName: String, symbolType: String?) async throws -> [String] { let store = try IndexStore(path: indexStorePath) - return try fileReferences(of: symbolName, symbolType: symbolType, from: store) + return try await fileReferences(of: symbolName, symbolType: symbolType, from: store) } func fileReferences( of symbolName: String, symbolType: String?, from store: some IndexStoreProviding & Sendable - ) throws -> [String] { + ) async throws -> [String] { let query = SymbolQuery(name: symbolName, kindString: symbolType) let index = RecordIndex.build(from: store) - return searchRecordsInParallel(store: store, index: index, query: query) + return await searchRecordsInParallel(store: store, index: index, query: query) } private func searchRecordsInParallel( store: some IndexStoreProviding & Sendable, index: RecordIndex, query: SymbolQuery - ) -> [String] { - let referencedFiles = ThreadSafeSet() - - DispatchQueue.concurrentPerform(iterations: index.recordNames.count) { i in - let recordName = index.recordNames[i] - - if recordContainsSymbol(store: store, recordName: recordName, query: query) { - let filename = index.sourcePath(for: recordName) - referencedFiles.insert(filename) + ) async -> [String] { + await withTaskGroup(of: String?.self) { group in + for recordName in index.recordNames { + group.addTask { + guard recordContainsSymbol(store: store, recordName: recordName, query: query) else { + return nil + } + return index.sourcePath(for: recordName) + } + } + + var referencedFiles = Set() + for await filename in group { + if let filename = filename { + referencedFiles.insert(filename) + } } + + return Array(referencedFiles).sorted() } - - return referencedFiles.values().sorted() } private func recordContainsSymbol( @@ -58,21 +65,3 @@ struct IndexStoreFinder { return found } } - -private final class ThreadSafeSet: @unchecked Sendable { - private let lock = NSLock() - private var storage = Set() - - func insert(_ element: Element) { - lock.lock() - storage.insert(element) - lock.unlock() - } - - func values() -> [Element] { - lock.lock() - let snapshot = Array(storage) - lock.unlock() - return snapshot - } -} diff --git a/Sources/SwiftFindRefs/SwiftFindRefs.swift b/Sources/SwiftFindRefs/SwiftFindRefs.swift index 6796382..e05043a 100644 --- a/Sources/SwiftFindRefs/SwiftFindRefs.swift +++ b/Sources/SwiftFindRefs/SwiftFindRefs.swift @@ -2,7 +2,7 @@ import ArgumentParser import Foundation @main -struct SwiftFindRefs: ParsableCommand { +struct SwiftFindRefs: AsyncParsableCommand { @Option(name: [.short, .customLong("projectName")], help: "The name of the Xcode project to help CLI find the Derived Data Index Store Path") var projectName: String? @@ -19,7 +19,7 @@ struct SwiftFindRefs: ParsableCommand { @Option(name: .shortAndLong, help: "Flag to enable verbose output.") var verbose: Bool = false - func run() throws { + func run() async throws { let fileSystem = FileSystem( fileManager: FileManager.default ) @@ -34,6 +34,6 @@ struct SwiftFindRefs: ParsableCommand { fileSystem: fileSystem, derivedDataLocator: derivedDataLocator ) - try compositionRoot.run() + try await compositionRoot.run() } } diff --git a/Tests/SwiftFindRefs/CompositionRootTests.swift b/Tests/SwiftFindRefs/CompositionRootTests.swift index 0b63f15..eea1110 100644 --- a/Tests/SwiftFindRefs/CompositionRootTests.swift +++ b/Tests/SwiftFindRefs/CompositionRootTests.swift @@ -7,7 +7,7 @@ struct CompositionRootTests { // MARK: - Tests @Test("test run with missing inputs throws missing inputs error") - func test_run_WithMissingInputs_throwsMissingInputsError() { + func test_run_WithMissingInputs_throwsMissingInputsError() async { // Given let fileSystem = MockFileSystem() let sut = makeSUT( @@ -18,8 +18,8 @@ struct CompositionRootTests { ) // When - let error = #expect(throws: DerivedDataLocatorError.self) { - try sut.run() + let error = await #expect(throws: DerivedDataLocatorError.self) { + try await sut.run() } // Then @@ -30,7 +30,7 @@ struct CompositionRootTests { } @Test("test run with invalid derived data path throws invalid path error") - func test_run_WithInvalidDerivedDataPath_throwsInvalidPathError() { + func test_run_WithInvalidDerivedDataPath_throwsInvalidPathError() async { // Given let invalidPath = "/invalid/DerivedData" let fileSystem = MockFileSystem(fileExistsResults: [invalidPath: false]) @@ -42,8 +42,8 @@ struct CompositionRootTests { ) // When - let error = #expect(throws: DerivedDataLocatorError.self) { - try sut.run() + let error = await #expect(throws: DerivedDataLocatorError.self) { + try await sut.run() } // Then @@ -55,7 +55,7 @@ struct CompositionRootTests { } @Test("test run with nil symbol type logs fallback before index store failure") - func test_run_WithNilSymbolType_logsFallbackBeforeIndexStoreFailure() throws { + func test_run_WithNilSymbolType_logsFallbackBeforeIndexStoreFailure() async throws { // Given let derivedDataPath = "/tmp/nonexistent/IndexStoreDB" let fileSystem = MockFileSystem(fileExistsResults: [derivedDataPath: true]) @@ -71,8 +71,8 @@ struct CompositionRootTests { ) // When - _ = #expect(throws: (any Error).self) { - try sut.run() + _ = await #expect(throws: (any Error).self) { + try await sut.run() } // Then diff --git a/Tests/SwiftFindRefs/IndexStore/IndexStoreFinderTests.swift b/Tests/SwiftFindRefs/IndexStore/IndexStoreFinderTests.swift index b3bdaed..7a549f7 100644 --- a/Tests/SwiftFindRefs/IndexStore/IndexStoreFinderTests.swift +++ b/Tests/SwiftFindRefs/IndexStore/IndexStoreFinderTests.swift @@ -9,33 +9,33 @@ struct IndexStoreFinderTests { // MARK: - fileReferences with invalid path @Test("test fileReferences with invalid path throws error") - func test_fileReferences_WithInvalidPath_throwsError() { + func test_fileReferences_WithInvalidPath_throwsError() async { // Given let sut = makeSUT(indexStorePath: "/nonexistent/index/store") // When / Then - #expect(throws: (any Error).self) { - _ = try sut.fileReferences(of: "SomeSymbol", symbolType: nil) + await #expect(throws: (any Error).self) { + _ = try await sut.fileReferences(of: "SomeSymbol", symbolType: nil) } } // MARK: - fileReferences with mock store @Test("test fileReferences with empty store returns empty array") - func test_fileReferences_WithEmptyStore_returnsEmptyArray() throws { + func test_fileReferences_WithEmptyStore_returnsEmptyArray() async throws { // Given let sut = makeSUT() let store = MockIndexStore(units: [], recordReaders: [:]) // When - let result = try sut.fileReferences(of: "SomeSymbol", symbolType: nil, from: store) + let result = try await sut.fileReferences(of: "SomeSymbol", symbolType: nil, from: store) // Then #expect(result.isEmpty) } @Test("test fileReferences with matching symbol returns file path") - func test_fileReferences_WithMatchingSymbol_returnsFilePath() throws { + func test_fileReferences_WithMatchingSymbol_returnsFilePath() async throws { // Given let sut = makeSUT() let symbolName = "MyClass" @@ -59,7 +59,7 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.count == 1) @@ -67,7 +67,7 @@ struct IndexStoreFinderTests { } @Test("test fileReferences with non-matching symbol returns empty array") - func test_fileReferences_WithNonMatchingSymbol_returnsEmptyArray() throws { + func test_fileReferences_WithNonMatchingSymbol_returnsEmptyArray() async throws { // Given let sut = makeSUT() let recordName = "SomeRecord" @@ -89,14 +89,14 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: "MySymbol", symbolType: nil, from: store) + let result = try await sut.fileReferences(of: "MySymbol", symbolType: nil, from: store) // Then #expect(result.isEmpty) } @Test("test fileReferences with matching name but different kind returns empty array") - func test_fileReferences_WithMatchingNameButDifferentKind_returnsEmptyArray() throws { + func test_fileReferences_WithMatchingNameButDifferentKind_returnsEmptyArray() async throws { // Given let sut = makeSUT() let symbolName = "Selection" @@ -119,14 +119,14 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: "class", from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: "class", from: store) // Then #expect(result.isEmpty) } @Test("test fileReferences with nil symbol type matches any kind") - func test_fileReferences_WithNilSymbolType_matchesAnyKind() throws { + func test_fileReferences_WithNilSymbolType_matchesAnyKind() async throws { // Given let sut = makeSUT() let symbolName = "MySymbol" @@ -150,7 +150,7 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.count == 1) @@ -158,7 +158,7 @@ struct IndexStoreFinderTests { } @Test("test fileReferences with multiple matching files returns sorted paths") - func test_fileReferences_WithMultipleMatchingFiles_returnsSortedPaths() throws { + func test_fileReferences_WithMultipleMatchingFiles_returnsSortedPaths() async throws { // Given let sut = makeSUT() let symbolName = "SharedProtocol" @@ -188,7 +188,7 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.count == 3) @@ -196,7 +196,7 @@ struct IndexStoreFinderTests { } @Test("test fileReferences skips system units") - func test_fileReferences_WithSystemUnits_skipsThem() throws { + func test_fileReferences_WithSystemUnits_skipsThem() async throws { // Given let sut = makeSUT() let symbolName = "MyClass" @@ -218,14 +218,14 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.isEmpty) } @Test("test fileReferences with unreadable record skips it") - func test_fileReferences_WithUnreadableRecord_skipsIt() throws { + func test_fileReferences_WithUnreadableRecord_skipsIt() async throws { // Given let sut = makeSUT() let symbolName = "MyClass" @@ -243,14 +243,14 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.isEmpty) } @Test("test fileReferences deduplicates files when symbol appears multiple times") - func test_fileReferences_WithDuplicateRecords_deduplicatesFiles() throws { + func test_fileReferences_WithDuplicateRecords_deduplicatesFiles() async throws { // Given let sut = makeSUT() let symbolName = "MyClass" @@ -280,7 +280,7 @@ struct IndexStoreFinderTests { ) // When - let result = try sut.fileReferences(of: symbolName, symbolType: nil, from: store) + let result = try await sut.fileReferences(of: symbolName, symbolType: nil, from: store) // Then #expect(result.count == 1) @@ -296,12 +296,12 @@ struct IndexStoreFinderTests { // MARK: - Test Doubles -private struct MockSymbol: SymbolMatching { +private struct MockSymbol: SymbolMatching, Sendable { let name: String let kind: SymbolKind } -private struct MockSymbolOccurrence: SymbolOccurrenceProviding { +private struct MockSymbolOccurrence: SymbolOccurrenceProviding, Sendable { let symbol: MockSymbol var symbolMatching: SymbolMatching { @@ -309,7 +309,7 @@ private struct MockSymbolOccurrence: SymbolOccurrenceProviding { } } -private struct MockRecordReader: RecordReaderProviding { +private struct MockRecordReader: RecordReaderProviding, Sendable { let occurrences: [MockSymbolOccurrence] func forEachOccurrence(_ callback: (SymbolOccurrenceProviding) -> Void) { @@ -317,13 +317,13 @@ private struct MockRecordReader: RecordReaderProviding { } } -private struct MockUnitDependency: UnitDependencyProviding { +private struct MockUnitDependency: UnitDependencyProviding, Sendable { let kind: DependencyKind let name: String let filePath: String } -private struct MockUnitReader: UnitReaderProviding { +private struct MockUnitReader: UnitReaderProviding, Sendable { let isSystem: Bool let dependencies: [MockUnitDependency] @@ -332,7 +332,7 @@ private struct MockUnitReader: UnitReaderProviding { } } -private struct MockIndexStore: IndexStoreProviding { +private struct MockIndexStore: IndexStoreProviding, Sendable { let units: [MockUnitReader] let recordReaders: [String: MockRecordReader] diff --git a/swiftfindrefs.rb b/swiftfindrefs.rb index c73c2fe..02a72a0 100644 --- a/swiftfindrefs.rb +++ b/swiftfindrefs.rb @@ -1,8 +1,8 @@ class Swiftfindrefs < Formula desc "SwiftFindRefs is a macOS Swift CLI that resolves a project’s DerivedData, reads Xcode’s IndexStore, and reports every file referencing a chosen symbol, with optional verbose tracing for diagnostics." homepage "https://github.com/michaelversus/SwiftFindRefs" - url "https://github.com/michaelversus/SwiftFindRefs.git", tag: "0.2.1" - version "0.2.1" + url "https://github.com/michaelversus/SwiftFindRefs.git", tag: "0.2.2" + version "0.2.2" depends_on "xcode": [:build]