--- name: axiom-swift-modern description: Use when reviewing or generating Swift code for modern idiom correctness — catches outdated APIs, pre-Swift 5.5 patterns, and Foundation legacy usage that Claude defaults to license: MIT --- # Modern Swift Idioms ## Purpose Claude frequently generates outdated Swift patterns from its training data. This skill corrects the most common ones — patterns that compile fine but use legacy APIs when modern equivalents are clearer, more efficient, or more correct. **Philosophy**: "Don't repeat what LLMs already know — focus on edge cases, surprises, soft deprecations." (Paul Hudson) ## Modern API Replacements | Old Pattern | Modern Swift | Since | Why | |-------------|-------------|-------|-----| | `Date()` | `Date.now` | 5.6 | Clearer intent | | `filter { }.count` | `count(where:)` | 5.0 | Single pass, no intermediate allocation | | `replacingOccurrences(of:with:)` | `replacing(_:with:)` | 5.7 | Swift native, no Foundation bridge | | `CGFloat` | `Double` | 5.5 | Implicit bridging; exceptions: optionals, inout, ObjC-bridged APIs | | `Task.sleep(nanoseconds:)` | `Task.sleep(for: .seconds(1))` | 5.7 | Type-safe Duration API | | `DateFormatter()` | `.formatted()` / `FormatStyle` | 5.5 | No instance management, localizable by default | | `String(format: "%.2f", val)` | `val.formatted(.number.precision(.fractionLength(2)))` | 5.5 | Type-safe, localized | | `localizedCaseInsensitiveContains()` | `localizedStandardContains()` | 5.0 | Handles diacritics, ligatures, width variants | | `"\(firstName) \(lastName)"` | `PersonNameComponents` with `.formatted()` | 5.5 | Respects locale name ordering | | `"yyyy-MM-dd"` with DateFormatter | `try Date(string, strategy: .iso8601)` | 5.6 | Modern parsing (throws); use "y" not "yyyy" for display | | `contains()` on user input | `localizedStandardContains()` | 5.0 | Required for correct text search/filtering | ## Modern Syntax | Old Pattern | Modern Swift | Since | |-------------|-------------|-------| | `if let value = value {` | `if let value {` | 5.7 | | Explicit `return` in single-expression | Omit `return`; `if`/`switch` are expressions | 5.9 | | `Circle()` in modifiers | `.circle` (static member lookup) | 5.5 | | `import UIKit` alongside `import SwiftUI` | Often not needed — SwiftUI re-exports most UIKit/AppKit types. Retain for UIKit-only APIs (`UIApplication`, etc.) | 5.5 | ## Foundation Modernization | Old Pattern | Modern Foundation | Since | |-------------|------------------|-------| | `FileManager.default.urls(for: .documentDirectory, ...)` | `URL.documentsDirectory` | 5.7 | | `url.appendingPathComponent("file")` | `url.appending(path: "file")` | 5.7 | | `books.sorted { $0.author < $1.author }` (repeated) | Conform to `Comparable`, call `.sorted()` | — | | `"yyyy"` in date format for display | `"y"` — correct in all calendar systems | — | ## SwiftUI Convenience APIs Claude Misses - **`ContentUnavailableView.search(text: searchText)`** (iOS 17+) automatically includes the search term — no need to compose a custom string - **`LabeledContent` in Forms** (iOS 16+) provides consistent label alignment without manual HStack layout - **`confirmationDialog()` must attach to triggering UI** — Liquid Glass morphing animations depend on the source element ## Swift 6.3 Concurrency Posture Write Swift 6.3-first code, not Swift 5-era code. These defaults apply to ALL new Swift code, not just when concurrency errors appear. | Default | Rationale | |---------|-----------| | Assume strict concurrency and MainActor default isolation for app/UI modules | Swift 6.3 language mode; Xcode 26+ default for new projects | | Prefer async/await over GCD, DispatchGroup, and callback pyramids | GCD is a bridge pattern for legacy APIs, not default architecture | | Async does not mean background — use `@concurrent` (Swift 6.2+) to force off-main | Async functions resume on the same actor they were called from | | Prefer structured concurrency (`async let`, `TaskGroup`) over unstructured `Task {}` | Structured tasks propagate cancellation and errors automatically | | Do not use `Task.detached` unless there is a specific, stated reason | Loses actor context, priority, and task-local values | | Prefer Sendable structs/enums for data that crosses actor boundaries | Value types are inherently safe to share | | Use actors only for truly shared mutable state across concurrency domains | Don't make every class an actor — UI code stays @MainActor | | Treat `@unchecked Sendable`, `@preconcurrency`, `nonisolated(unsafe)` as temporary bridge tools | Each should have a removal ticket, not be permanent | | Do not add escape hatches just to silence compiler errors | They hide data races that crash in production | For detailed patterns, decision trees, and error-specific guidance, see `axiom-swift-concurrency`. ## Common Claude Hallucinations These patterns appear frequently in Claude-generated code: 1. **Creates `DateFormatter` instances inline** — Use `.formatted()` or `FormatStyle` instead. If a formatter must exist, make it `static let`. 2. **Uses `DispatchQueue.main.async`** — Use `@MainActor` or `MainActor.run`. GCD is a bridge pattern, not a default. 3. **Uses `DispatchQueue.global().async` for background work** — Use `@concurrent` (Swift 6.2+) or extract to an actor. 4. **Uses `Task.detached` to "make it background"** — Use `@concurrent`. `Task.detached` loses actor context. 5. **Uses `CGFloat` for SwiftUI parameters** — `Double` works everywhere since Swift 5.5 implicit bridging. 6. **Generates `guard let x = x else`** — Use `guard let x else` shorthand. 7. **Returns explicitly in single-expression computed properties** — Omit `return`. 8. **Spawns unstructured `Task {}` in loops** — Use `TaskGroup` for dynamic parallel work. 9. **Adds `@unchecked Sendable` to silence warnings** — Convert to actor or proper Sendable type. ## Resources **Skills**: axiom-swift-performance, axiom-swift-concurrency, axiom-swiftui-architecture