programming-swift · diff
git:20260509.966244f to git:20260510.fe97ca3
45 added, 54 removed. Audit A to A.
---
name: programming-swift
title: "Swift Development"
- description: "Swift 6 conventions, concurrency, SPM, macros, testing, and idiomatic patterns. Auto-activates in Swift package projects."
+ description: "Modern Swift architecture: value types, concurrency, protocol-oriented design, and SPM. Auto-activates in Swift package projects."
license: Apache-2.0
compatibility: "Requires Swift 6.0+ toolchain."
capabilities: programming-swift
domains: developer
rules:
- file(Package.swift)
- content(swift)
---
- ## Conventions
+ ## Mental model
- - Swift 6 language mode for strict concurrency — opt into complete data-race safety (set `swiftLanguageMode: .v6` in Package.swift)
- - Value types preferred — struct over class unless reference semantics required
- - Protocol-oriented — protocol + extension over inheritance hierarchies
- - Access control — default to private/internal, explicit public API surface
- - @Sendable and actor isolation — mark closures and types crossing isolation boundaries
- - Swift Testing over XCTest for new test targets
- - Formatted — code must follow swift-format style
- - SPM (Package.swift) for dependency management — no CocoaPods/Carthage for new projects
+ Swift is a value-oriented language with strong static typing and modern concurrency. The maintainable Swift codebase keeps domain logic in `struct`s and `enum`s, uses classes only for identity or shared mutable state, and isolates concurrency boundaries with actors and `@MainActor`. Reaching for class hierarchies and reference semantics by default produces fragile, hard-to-test code.
- ## Concurrency
+ ## Value-oriented design
- - Strict concurrency checking — Swift 6 language mode enforces Sendable at compile time, data races become errors
- - Actors for mutable shared state — prefer actor over class + lock
- - async/await for all asynchronous code — no completion handlers in new code
- - TaskGroup for structured parallel work — child tasks scoped to parent lifetime
- - AsyncSequence / AsyncStream for event streams and bridging callback APIs
- - @MainActor for UI-bound code — isolate all UI state mutations
- - Sendable conformance — value types are Sendable by default, audit reference types
- - Never block an actor — offload CPU-heavy work with a custom executor or explicit dispatch; Task.detached only to drop actor isolation inheritance
- - Task cancellation — check Task.isCancelled, use try Task.checkCancellation() cooperatively
+ - `struct` by default; reach for `class` only when you need identity, shared state, or Obj-C interop
+ - `enum` with associated values models finite, exhaustive cases — pattern-match instead of branching on optional fields
+ - Protocol-oriented composition: small protocols + extensions beat deep inheritance hierarchies
+ - Generics with associated types (`some Protocol`, `any Protocol`) let APIs stay value-typed while remaining flexible
+ - Avoid `AnyObject`-only protocols unless you genuinely need reference semantics
- ## Type System
+ ## Concurrency (Swift 6)
- - Typed throws (Swift 6) — `func parse() throws(ParseError)` for precise error signatures (single error type only)
- - Noncopyable types (~Copyable) — single-ownership semantics for structs and enums only (classes cannot be ~Copyable)
- - @Observable macro (Observation framework) — replaces ObservableObject/@Published
- - Opaque types (`some Protocol`) for stable API return types — prefer over existentials
- - Existential types (`any Protocol`) only when runtime polymorphism is needed
- - Result builders for DSL construction — SwiftUI, RegexBuilder
+ - Strict concurrency checking enforces `Sendable` at compile time — design data flow so values cross actor boundaries, not shared references
+ - Actors own mutable state — replace `class + lock` patterns with an `actor` that exposes async methods
+ - `@MainActor` for all UI-bound state and code; main-actor-isolated types can't be passed across boundaries without `await`
+ - `async`/`await` only — no completion handlers in new code; bridge old APIs with `withCheckedContinuation`
+ - Structured concurrency via `TaskGroup` / `async let` — child tasks are bounded by the parent; prefer over `Task { }` detached work
+ - `Task.detached` only when you need to escape actor inheritance — it's an exception, not a default
- ## Macros
+ ## Error handling
- - Swift macros generate code at compile time — significant boilerplate reduction
- - @attached macros modify declarations (@Observable, @Model), @freestanding macros are standalone expressions (#Predicate, #Preview)
- - Use built-in macros first: @Observable, #Predicate, #Preview, @Model
- - Custom macros require separate macro target in Package.swift with SwiftSyntax dependency
- - Test macros with assertMacroExpansion — verify generated code
+ - `throws` for recoverable failures; typed throws (`throws(MyError)`) for library APIs where callers benefit from knowing the exact error type
+ - `Result<T, E>` for stored errors, callback bridges, and crossing async boundaries when needed
+ - `try?` only when the error genuinely doesn't matter; `try!` belongs in tests and prototypes, never in shipping paths
+ - Don't use exceptions for control flow — model expected outcomes as enum cases or `Result`
- ## Error Handling
+ ## API design
- - throws for recoverable errors — catch at appropriate call-site boundary
- - Typed throws for library APIs — callers know exact error types without docs
- - Result<T, E> for stored errors and async callback bridges
- - try? for optional conversion when error details unneeded
- - try! only in tests/prototypes — never in production paths
- - Never fatalError in library code — return Result or throw
+ - Methods read like English at the call site — the first argument label completes the verb (`array.insert(x, at: 0)`)
+ - Initializers and factories are deliberate: provide the few configurations users actually need, not every permutation
+ - `@Observable` (Observation framework) replaces `ObservableObject`/`@Published` for new SwiftUI code
+ - Prefer `some Protocol` return types for stable APIs; `any Protocol` only when runtime polymorphism is required
+ - Mark internal types `internal` (the default) — `public` is an opt-in commitment
- ## Testing
+ ## SwiftUI patterns
- - Swift Testing framework (@Test, #expect, @Suite) for all new test code
- - XCTest for UI tests (XCUIApplication), performance tests (XCTMetric), and Objective-C test code
- - #expect(expression) over XCTAssertEqual — better diagnostics and failure messages
- - @Test(.tags(.networking)) for trait-based organization and selective execution
- - @Test(arguments: cases) for parameterized data-driven tests
- - Both frameworks coexist — swift test runs both Swift Testing and XCTest targets
+ - `NavigationStack` with value-based `NavigationLink` for type-safe routing
+ - `@Observable` model classes + `@Bindable` for two-way binding in views
+ - `.task` modifier for async work tied to view lifetime — cancels automatically
+ - Keep views small; push logic into observable models or pure functions
+ - Previews (`#Preview`) double as living documentation — make them work across data states
- ## Performance
+ ## Project layout and SPM
- - Value types (struct, enum) — stack-allocated, no reference counting overhead
- - Copy-on-write for large value types — use isKnownUniquelyReferenced for custom COW
- - Generics over protocol existentials in hot paths — enables compiler specialization
- - @inlinable for performance-critical public library functions
- - Lazy collections (lazy.map, lazy.filter) for chained transformations on large data
- - Avoid unnecessary heap allocations — prefer stack storage and inline buffers
+ - Swift Package Manager for everything new — no CocoaPods/Carthage
+ - Split into targets along boundaries that compile and test independently; resist the "one big target" temptation
+ - Resources, plugins, and macros each get their own target
+ - Use `swiftLanguageMode: .v6` in `Package.swift` to opt into strict concurrency from day one
+
+ ## Testing
+
+ - Swift Testing (`@Test`, `#expect`, `@Suite`) for new test code — better diagnostics, trait-based organization, parameterized tests
+ - XCTest only for UI automation (`XCUIApplication`) and Obj-C interop
+ - Test value types directly — no mocks needed; for protocols, provide a hand-written conforming type
+ - `#expect(throws: MyError.self) { try someCall() }` for error paths