--- name: swift-idioms description: >- Swift idioms and best practices: value types, Optionals hygiene, protocol-oriented design, async/await and Actor concurrency, and XCTest. Use when writing, refactoring, or reviewing Swift iOS, macOS, or server-side code. --- ## Swift Idioms and Patterns Swift rewards value types, optionals, and protocol-oriented design. Idiomatic Swift = safe, expressive, Swifty. > Scope: Swift coding idioms. Test naming: .agents/rules/testing-strategy.md. ### Value Types and Optionals 1. **Prefer structs over classes** — value semantics by default. Classes only for identity, inheritance, or reference counting. 2. **Optionals — never force-unwrap (`!`) in production:** ```swift // ✅ Guard let for early exit guard let task = storage.findById(id) else { throw TaskError.notFound(id) } // ✅ Optional chaining let title = task?.title ?? "Untitled" // ✅ if let for conditional binding if let deadline = task.deadline { scheduleReminder(for: deadline) } // ❌ Force unwrap — crash risk let task = storage.findById(id)! ``` 3. **Property wrappers** for reusable behavior: ```swift @propertyWrapper struct Clamped { var wrappedValue: Value { didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) } } let range: ClosedRange init(wrappedValue: Value, _ range: ClosedRange) { self.range = range self.wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) } } struct Task { @Clamped(0...100) var progress: Int = 0 } ``` ### Error Handling > For universal error handling principles, see `.agents/rules/error-handling-principles.md`. 1. **Typed throws (Swift 6) or `Error` protocol:** ```swift enum TaskError: Error, LocalizedError { case notFound(String) case validationFailed(field: String, message: String) case storageUnavailable var errorDescription: String? { switch self { case .notFound(let id): "Task '\(id)' not found" case .validationFailed(let field, let msg): "Validation failed on \(field): \(msg)" case .storageUnavailable: "Storage is unavailable" } } } func getTask(id: String) throws(TaskError) -> Task { ... } ``` 2. **`Result` type for async callbacks** (pre-async/await): ```swift func fetchTask(id: String) async -> Result { ... } ``` 3. **`do`/`catch` with pattern matching:** ```swift do { let task = try getTask(id: "123") process(task) } catch TaskError.notFound(let id) { logger.warn("Task not found", metadata: ["taskId": id]) } catch { logger.error("Unexpected error", metadata: ["error": "\(error)"]) } ``` 4. **`defer` for cleanup:** ```swift func processFile(at path: String) throws -> Data { let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path)) defer { handle.closeFile() } // ✅ Always runs on exit return handle.readDataToEndOfFile() } ``` ### Protocol-Oriented Design 1. **Protocols over abstract classes:** ```swift // ✅ Interface defined as protocol protocol TaskStorage { func getById(_ id: String) async throws -> Task? func save(_ task: Task) async throws } // ✅ Production implementation struct PostgresTaskStorage: TaskStorage { let pool: ConnectionPool func getById(_ id: String) async throws -> Task? { try await pool.query("SELECT * FROM tasks WHERE id = $1", [id]).first } func save(_ task: Task) async throws { try await pool.execute("INSERT INTO tasks ...", [task.id, task.title]) } } // ✅ Test implementation struct MockTaskStorage: TaskStorage { var tasks: [Task] = [] func getById(_ id: String) async throws -> Task? { tasks.first { $0.id == id } } func save(_ task: Task) async throws { tasks.append(task) } } ``` 2. **Protocol extensions for default implementations:** ```swift protocol Identifiable { var id: String { get } } extension Identifiable { var isNew: Bool { id.isEmpty } } ``` 3. **Associated types for generic protocols:** ```swift protocol Repository { associatedtype Entity func findById(_ id: String) async throws -> Entity? func save(_ entity: Entity) async throws } ``` ### Concurrency 1. **Structured concurrency with `async`/`await`:** ```swift func loadDashboard() async throws -> Dashboard { async let user = fetchUser(id) async let tasks = fetchTasks(userId: id) async let stats = fetchStats() return Dashboard( user: try await user, tasks: try await tasks, stats: try await stats ) } ``` 2. **`@Sendable`** for closures crossing concurrency domains. 3. **Actors** for thread-safe mutable state: ```swift actor TaskCache { private var cache: [String: Task] = [:] func get(_ id: String) -> Task? { cache[id] } func set(_ id: String, task: Task) { cache[id] = task } func invalidate(_ id: String) { cache.removeValue(forKey: id) } } // ✅ Safe concurrent access let cache = TaskCache() await cache.set("123", task: newTask) if let task = await cache.get("123") { ... } ``` 4. **`TaskGroup`** for dynamic concurrency: ```swift func fetchAllTasks(ids: [String]) async throws -> [Task] { try await withThrowingTaskGroup(of: Task.self) { group in for id in ids { group.addTask { try await fetchTask(id: id) } } return try await group.reduce(into: []) { $0.append($1) } } } ``` ### Naming (Swift API Design Guidelines) 1. **camelCase** for functions, properties, variables. 2. **PascalCase** for types, protocols, enums. 3. **Omit needless words** — `remove(at:)` not `removeItem(atIndex:)`. 4. **Protocols for capabilities** use `-able`/`-ible`: `Codable`, `Identifiable`. 5. **Factory methods** use `make` prefix: `makeIterator()`. 6. **Boolean properties** read as assertions: `isEmpty`, `hasChanges`, `isValid`. ### Anti-Patterns - ❌ **Force unwrap (`!`) in production code** — crashes at runtime - ❌ **`var` when `let` suffices** — always prefer immutability - ❌ **Classes when structs work** — unnecessary reference semantics - ❌ **Stringly-typed APIs** — use enums for finite option sets - ❌ **Massive view controllers** — extract to view models, coordinators - ❌ **`try?` silently discarding errors** — log or handle the error case - ❌ **Nested `if let` pyramids** — use `guard let` for early returns ```swift // ❌ Pyramid of doom if let user = getUser() { if let tasks = getTasks(for: user) { if let first = tasks.first { process(first) } } } // ✅ Flat with guard guard let user = getUser() else { return } guard let tasks = getTasks(for: user), let first = tasks.first else { return } process(first) ``` ### Testing XCTest or Swift Testing (6.0+). Mock via protocols. ```swift // XCTest final class TaskServiceTests: XCTestCase { func testGetTask_returnsNotFound() async throws { let storage = MockTaskStorage() let service = TaskService(storage: storage) do { _ = try await service.getTask(id: "999") XCTFail("Expected notFound error") } catch TaskError.notFound(let id) { XCTAssertEqual(id, "999") } } } // Swift Testing (6.0+) @Test func getTask_returnsNotFound() async throws { let storage = MockTaskStorage() let service = TaskService(storage: storage) #expect(throws: TaskError.notFound("999")) { try await service.getTask(id: "999") } } ``` ### Formatting and Static Analysis | Tool | Purpose | Command | |---|---|---| | `swift-format` | Formatting | `swift-format -i -r Sources/` | | SwiftLint | Linting | `swiftlint lint --strict` | | Xcode Analyzer | Static analysis | Built-in | ### Related - Code Idioms and Conventions .agents/rules/code-idioms-and-conventions.md - Testing Strategy .agents/rules/testing-strategy.md - Error Handling Principles .agents/rules/error-handling-principles.md - Concurrency and Threading Principles @.agents/rules/concurrency-and-threading-principles.md