swift-idioms

SkillAI & models

Swift rewards value types, optionals, and protocol-oriented design. Idiomatic Swift = safe, expressive, Swifty.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the swift-idioms skill

About this capability

Comprehensive sets of standards and practices designed to elevate the capabilities of AI coding agents.

What this skill tells your AI

The instructions your AI receives, as published by irahardianto/awesome-agv in .agents/skills/swift-idioms/SKILL.md and read by ahel’s review.

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:

    // ✅ 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:

    @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.

  1. Typed throws (Swift 6) or Error protocol:

    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):

    func fetchTask(id: String) async -> Result<Task, TaskError> { ... }
    
  3. do/catch with pattern matching:

    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:

    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:

    // ✅ 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:

    protocol Identifiable {
        var id: String { get }
    }
    
    extension Identifiable {
        var isNew: Bool { id.isEmpty }
    }
    
  3. Associated types for generic protocols:

    protocol Repository {
        associatedtype Entity
        func findById(_ id: String) async throws -> Entity?
        func save(_ entity: Entity) async throws
    }
    

Concurrency

  1. Structured concurrency with async/await:

    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:

    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:

    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 wordsremove(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
// ❌ 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

ToolPurposeCommand
swift-formatFormattingswift-format -i -r Sources/
SwiftLintLintingswiftlint lint --strict
Xcode AnalyzerStatic analysisBuilt-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

Signals

GitHub stars
156
Forks
53
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
swift-idioms
Source
github.com/irahardianto/awesome-agv