---
name: ios-coding-standard
description: "The iOS coding-standard rule registry: 99 stable-ID rules across readability, security, service layer, business rules, concurrency, testing, module boundaries, naming and visibility, each with a severity and enforcement kind. Use when writing or reviewing Swift and you need the project rule rather than an opinion, or on a persistence, logging or business-rule-placement question."
user-invocable: true
standards-registry: references/rules.yml
---

# iOS coding standard

The registry is `references/rules.yml`. It is the source of truth, and it is
the reason to load this skill: a rule cited by ID is checkable and a remembered rule
is not.

| File | What it is | When to read it |
|---|---|---|
| `references/rules.yml` | 99 rules with stable IDs, severity, enforcement kind and a `check` describing what counts as a violation | before asserting that something is or is not a violation |
| `references/STANDARD.md` | the same rules taught with before/after Swift, for a human | when you need the reasoning or an example, not just the rule |
| `references/swiftlint.draft.yml` | the mechanically-enforceable subset as a SwiftLint config | when wiring lint into a project |
| `references/lint-local.sh` | runs that config over one module, with a baseline mode | when grandfathering existing violations so only new ones surface |
| `modules/<Module>.yml` | per-module overlay: vocabulary bindings, dialect choices, zero-instance prohibitions, carve-outs, known findings. **Project-local, never shipped**  -  see `modules/_TEMPLATE.yml` | before auditing a module, and before raising any slot-bound rule |
| `references/EXAMPLES.md` | optional, project-local: a worked ✗/✓ pair per judgement rule, keyed by ID, drawn from real code in YOUR repo | when citing a judgement rule, so the fix and the audit read the same picture |

**Two files are deliberately absent from the shipped skill.** `modules/<Module>.yml` and
`references/EXAMPLES.md` quote a specific codebase: module paths, symbol names, real call-site
counts, before/after snippets. That is exactly what makes them useful and exactly why they cannot
travel  -  a shipped overlay would bind slots to another project's dialect, and a shipped example
would teach its naming. Write them in your own installation; `modules/_TEMPLATE.yml` carries the
shape and the slot list. An absent overlay is not a neutral default: an unbound slot DISABLES its
rules, and the audit reports that rather than guessing a dialect.

## How to use it

1. **Cite, do not paraphrase.** Reference `SEC-04`, `READ-01`, `SVC-02`. A finding
   without an ID is an opinion, and the author cannot look it up to disagree.
2. **A rule not in the registry is not a rule.** If a review wants one, propose it
   as `status: proposed` with a rationale rather than enforcing it silently.
3. **Severity decides whether it blocks.** `blocking` stops the change,
   `important` is fixed in the same pass, `suggestion` is optional. Do not promote a
   suggestion to blocking because it happens to bother you.
4. **Enforcement kind decides who checks it.** `format` belongs to the formatter,
   `lint` to the linter, `scan` to a tool, `judgement` to a person or an audit run.
   Reviewing a `format` rule by hand wastes the review.
5. **Exceptions are marked in code, with an expiry:**
   `// standard:exception(<RULE-ID>) <reason> <expiry:YYYY-MM-DD>`. An unmarked
   deviation is a finding; a marked one is a decision.

## Three decisions the registry answers, and code usually gets wrong

**Persistence.** Read `references/rules.yml → persistence_decision` before reaching for
storage. The ladder starts at "does this value need to outlive the current flow?"
with the default answer *no*. The Keychain answers "where does a persisted secret
live", not "this value is sensitive" - most sensitive values in a flow never need to
persist, and persisting them is the more expensive mistake, because the item
outlives the flow and the cleanup belongs to nobody.

**What a mapper may do.** A mapper lowers one shape onto another. Unwrapping an
optional wire field to an empty value is lowering. Arithmetic and unit conversion, a
clamp or threshold, choosing a screen state, and policy defaults are **not** - each
belongs to the view model, or to a named domain rule when several screens share it.
Carry the wire value with its unit in the name and convert where it is read.

**Where a business rule lives.** Read `references/rules.yml → RULE-01` before writing one. Three
homes, cheapest rung first: the **view model** when one screen asks it  -  stop there, a namespace with
one consumer is over-hoisting; a **computed property on the entity** when several screens ask it of
data the entity already carries; a **named rule namespace** when several screens ask it but the
answer is screen policy rather than entity state. `SVC-08` bans every other layer. The part usually
skipped is the citation: a rule names the requirement it enforces and its test repeats that token, so
one grep reaches requirement, code and test  -  and that citation is what makes a `judgement` rule
checkable instead of an opinion.

## Rule families

`READ` readability and section structure · `SEC` secrets, logging, storage ·
`SVC` service layer and mapping · `RULE` business-rule surface and traceability ·
`TEST` testability seams · `MOD` module
boundaries and imports · `NAME` naming · `FLEX` flexibility and extension points ·
`CONC` concurrency · `VIS` visibility and access level · `UI` view construction ·
`PERF` performance · `DEPR` deprecation and retirement.

## Scope note

The registry is project-configurable, not project-specific: rule bodies describe
shapes (a mapper doing arithmetic, a logger interpolating a token) rather than named
modules. A project adds its own vocabulary through a `modules/<Module>.yml` overlay
in the `modules/` directory - the module's terms, its allowed dependencies, its validation
rules - which the audit binds on top of the shared registry. Nothing in the shared
registry names a module, so it applies unchanged to any SwiftUI codebase.
Copy `modules/_TEMPLATE.yml` to `modules/<YourModule>.yml` and bind the slots from evidence in your
own code  -  each binding wants a count, not a preference, because the count is what tells a dialect
choice apart from a defect.

**Language scope is declared, not assumed.** The registry carries a top-level `scope:` block naming
the languages and paths its rules apply to. This exists because the rule bodies below were written
against Swift with SwiftUI view construction: applied unread to an Objective-C or UIKit file they
would manufacture findings, which buries the real ones and teaches the reader to distrust the run.
A consumer that cannot honour `scope` must load the registry as reference only and say so.

**Some rules govern a choice, not a defect.** Where two shapes are each internally coherent and the
only real cost is mixing them, the rule binds to a slot in `rules.yml → module_overlay_slots` -
service-method naming, the navigation-exit spelling, the component directory's name. An **unbound
slot disables its rules**, and the audit says so rather than defaulting to one dialect silently.
This matters because the alternative is reporting a hundred findings against a module that made the
other defensible choice, which is a migration proposal wearing a standards pass. A slot is only
legitimate when both values are genuinely defensible: `UI-04` is the counter-example - a module with
no copy surface has not chosen differently, it is missing the surface, so that rule is never slotted.

`references/swiftlint.draft.yml` carries one placeholder worth changing per project: the remote
image-host allowlist is set to `example.com`. Point it at your own CDN, or the rule
flags every remote image.

## Running a full audit

For a module-wide pass with a plan and a handoff, the pipeline ships
`/multi-agent:ios-coding-standard`, which builds the module registry, binds the
overlay, applies the surviving rules and produces a remediation plan. This skill is
the registry that audit reads; use the skill directly when you are writing or
reviewing code rather than auditing a whole module.
