irahardianto / swift-idioms
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
- Prefer structs over classes — value semantics by default. Classes only for identity, inheritance, or reference counting.
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)!`Property wrappers for reusable behavior:
`swift
@propertyWrapper
struct Clamped<Value: Comparable> {
var wrappedValue: Value {
didSet { wrappedValue = min(max(wrappedValue, range.lowerBound), range.upperBound) }
}
let range: ClosedRange<Value>init(wrappedValue: Value, _ range: ClosedRange<Value>) {
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.
Typed throws (Swift 6) or
Errorprotocol:`swift
enum TaskError: Error, LocalizedError {
case notFound(String)
case validationFailed(field: String, message: String)
case storageUnavailablevar 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 { ... }
`Resulttype for async callbacks (pre-async/await):`swift
func fetchTask(id: String) async -> Result<Task, TaskError> { ... }`do/catchwith 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)"])
}`deferfor cleanup:`swift
func processFile(at path: String) throws -> Data {
let handle = try FileHandle(forReadingFrom: URL(fileURLWithPath: path))
defer { handle.closeFile() } // ✅ Always runs on exitreturn handle.readDataToEndOfFile()
}`
Protocol-Oriented Design
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: ConnectionPoolfunc 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)
}
}`Protocol extensions for default implementations:
`swift
protocol Identifiable {
var id: String { get }
}extension Identifiable {
var isNew: Bool { id.isEmpty }
}`Associated types for generic protocols:
`swift
protocol Repository {
associatedtype Entity
func findById(_ id: String) async throws -> Entity?
func save(_ entity: Entity) async throws
}`
Concurrency
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
)
}`@Sendablefor closures crossing concurrency domains.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") { ... }`TaskGroupfor 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)
- camelCase for functions, properties, variables.
- PascalCase for types, protocols, enums.
- Omit needless words —
remove(at:)notremoveItem(atIndex:). - Protocols for capabilities use
-able/-ible:Codable,Identifiable. - Factory methods use
makeprefix:makeIterator(). - Boolean properties read as assertions:
isEmpty,hasChanges,isValid.
Anti-Patterns
- ❌ Force unwrap (
!) in production code — crashes at runtime - ❌
varwhenletsuffices — 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 letpyramids — useguard letfor early returns
// ❌ 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.
// 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