Skip to content
Back to skills

Swift Conventions

ASecurity

Use when a ticket adds or changes Swift code — iOS/macOS apps, SwiftUI or UIKit, server-side Swift — and it must follow the repo's Swift conventions — the Swift API Design Guidelines, value types and protocols, optionals handled without force-unwraps, Swift 6 strict concurrency (async/await, actors, Sendable) used correctly, SwiftLint/swift-format clean — as the language pack for any Swift change. Invoke for "build this screen in SwiftUI", "fix the retain cycle", "remove the force unwraps", "...

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 28, 2026
ai-agentsgoswifttestingapidatabasesecuritydocumentation

Works with

  • api
  • mcp

Security analysis

A100/100

Scanned September 29, 2026

npx -y skills add tmj-90/gaffer --skill swift-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Swift Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Swift Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tmj-90-swift-conventions/badge)](https://www.skillsdirectory.com/skills/tmj-90-swift-conventions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: swift-conventions
description: Use when a ticket adds or changes Swift code — iOS/macOS apps, SwiftUI or UIKit, server-side Swift — and it must follow the repo's Swift conventions — the Swift API Design Guidelines, value types and protocols, optionals handled without force-unwraps, Swift 6 strict concurrency (async/await, actors, Sendable) used correctly, SwiftLint/swift-format clean — as the language pack for any Swift change. Invoke for "build this screen in SwiftUI", "fix the retain cycle", "remove the force unwraps", "fix the Sendable/data-race errors", or when reviewing a Swift diff.
stack: [swift, ios, macos]
area: language
---

# Write idiomatic Swift

Swift is safest when values are structs, absence is an optional you handle, and
concurrency is checked by the compiler through actors and `Sendable`. For the builder and the reviewer of a Swift diff; the target's settings and existing code win over it.

## Procedure

1. **Discover the target's conventions first.** Call `search_lore` for Swift conventions.
   Read `Package.swift` (`swift-tools-version`, `swiftLanguageModes`, `swiftSettings`
   such as upcoming features or `defaultIsolation(MainActor.self)`), or the Xcode build
   settings (`SWIFT_VERSION`, `SWIFT_STRICT_CONCURRENCY`, `SWIFT_DEFAULT_ACTOR_ISOLATION`,
   approachable-concurrency flags), `project.yml` (XcodeGen/Tuist), `.swiftlint.yml`,
   `.swift-format`, and the minimum OS. The language mode decides the rules: in Swift 6
   mode data-race diagnostics are errors; with default MainActor isolation (6.2) code is
   main-actor unless marked otherwise. Identify SwiftUI vs UIKit, the architecture
   (`@Observable` MVVM, TCA), and XCTest vs Swift Testing. Copy a sibling feature and test.
2. **Pin the exact commands** from CI, `Makefile` or fastlane (the `run-tests` and
   `run-lint` skills), including the scheme and simulator destination. Use the defaults
   below only when the repo defines none.
3. **Write the change with the idioms below**, then walk the concurrency section for every
   task, actor, closure, file and shared object you touched.
4. **Test each acceptance criterion's own behaviour.** One test per AC that fails without
   your change, plus its error path (`@Test(arguments:)` for tables). If the AC involves
   shared state or persistence, add a test that runs N concurrent calls in a
   `withTaskGroup` and asserts the invariant.
5. **Verify, then stop.** Done when: the build has no new warnings in the configured
   language mode, SwiftLint/swift-format are clean, tests are green on the repo's
   destination, and every AC has a test. Record the output with the `record-evidence`
   skill; the runner submits the work.

## Commands

- SwiftPM: `swift build` and `swift test` (one test: `swift test --filter Suite/test`).
  Never `swift package update` or edit `Package.resolved`; a new or bumped package is a
  blocker (the `dependency-upgrade` skill).
- Xcode: `xcodebuild -list`, then `xcodebuild test -scheme <Scheme> -destination
  '<CI destination>'`.
- Lint: `swiftlint lint --strict` (if configured); `swift format lint --strict
  --recursive Sources Tests` (swift-format ships with the Swift 6 toolchain).

## Idioms that matter

- **API Design Guidelines**: clarity at the point of use; name by role, not type; omit
  needless words; argument labels that read as phrases (`insert(_:at:)`); mutating/
  non-mutating pairs `sort()`/`sorted()`; Booleans read as assertions (`isEmpty`); a
  documentation comment on every public declaration; note complexity when a computed
  property is not O(1).
- **Value types first**: `struct`/`enum` for models and state; `class` only for identity,
  and `final` unless designed for subclassing; protocols with extensions over inheritance.
- **Optionals**: `guard let` with early return, `??` with a real default, optional
  chaining. No `!`, `try!` or implicitly unwrapped optionals on reachable paths (only
  `@IBOutlet`).
- **Errors**: `throws` with a module error enum (typed `throws(MyError)` where the repo uses
  it), handled at the boundary; `try?` only where absence is acceptable, with a comment.
- **Exhaustive `switch`** on enums the module owns, with no `default`.
- **SwiftUI**: small views; state owned at the right level (`@State` local, `@Binding`
  down, `@Observable` models injected); `.task` for async work tied to view lifetime; no
  side effects in `body`.

## Concurrency and resource safety

- **Isolation is the lock.** UI state is `@MainActor`; shared mutable state lives in an
  `actor`. `Sendable` conformances are honest: no `@unchecked Sendable`,
  `nonisolated(unsafe)` or `MainActor.assumeIsolated` without a written proof.
- **Actor reentrancy**: an actor method that awaits can be interleaved with other calls on
  the same actor, so state read before an `await` may be stale after it — a lost update
  inside an actor. Re-check invariants after each `await`, or finish the read-modify-write
  synchronously before suspending, or dedupe in-flight work with a stored `Task`.
- **Unstructured tasks** (`Task { }`, `Task.detached`) are not cancelled with their creator:
  keep the handle and cancel it (in `deinit`, `onDisappear`, or by using `.task`). Prefer
  `async let` and task groups. Long loops call `try Task.checkCancellation()`.
- **Offloading work**: CPU-heavy or blocking work does not run on the main actor; mark it
  `@concurrent` (6.2) or move it to a non-main actor. Never `DispatchQueue.main.sync`
  from the main thread; never block a task on a semaphore waiting for another task.
- **Continuations** (`withCheckedThrowingContinuation`) resume exactly once on every path.
- **Memory**: `[weak self]` in escaping closures stored by objects that `self` owns
  (timers, observers, publishers); break parent/child reference cycles; `defer` to release
  resources.
- **Files other readers use**: write with `data.write(to: url, options: .atomic)` (temp file
  plus rename) or `FileManager.replaceItemAt`; never a fixed temp name. Cross-process
  exclusion needs a file lock or the database; a time-only lease lets a suspended holder
  write late, so writes must check a version.

## Review checklist — flag as defects

Walk this against the diff. An item is grounds for CHANGES only when, in changed code, it
causes a concrete failure (wrong result, crash, lost or corrupted data, security hole) or
leaves an AC's own behaviour untested: cite the line and that failure. Otherwise it is an
`(optional)` note. Formatting the tools would fix, and preferences the repo
does not enforce, are not findings. Do not patch the code under review.

- [ ] `!`, `try!` or an implicitly unwrapped optional on a reachable path; `try?` silently
      discarding an error the caller needed.
- [ ] `@unchecked Sendable`, `nonisolated(unsafe)` or a lowered strict-concurrency setting
      without a proof; UI state mutated off the main actor.
- [ ] Actor state read before an `await` and written after it without re-checking.
- [ ] An unstructured `Task` never cancelled; missing cancellation checks in long work.
- [ ] Blocking or heavy work on the main actor; `DispatchQueue.main.sync` or a semaphore
      wait inside async code; a continuation resumed zero or two times.
- [ ] A reference cycle through an escaping closure or delegate held strongly.
- [ ] A shared file written in place or via a fixed temp name; a time-only lock lease.
- [ ] `default` on a `switch` over an enum the module owns, hiding a future case.
- [ ] A new SwiftLint `disable` without a reason.
- [ ] An AC has no test, or an async test waits with sleeps instead of awaiting or
      `confirmation`/expectations.

## Capture lore

This skill is one of the places durable, reusable knowledge naturally surfaces:
**While matching the target you learn the architecture pattern, the DI approach and the concurrency conventions the app relies on.** That kind of fact is *lore*. Capture it via the **lore-capture
protocol in your brief** (`CLAUDE.factory.md`, step 11 "Memory contribution"):
call the Memory MCP `suggest_lore` once at the close of your work — reusable
conventions, gotchas, decisions, and boundaries only, never per-ticket trivia.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…