Audit planning docs, story files, and Swift source code against the Swift/SwiftUI guidance in docs/setup/swift/. Produces a remediation story file with one AC per finding. Use when the user says "swift audit", "audit swift", "audit codebase", or "check swift patterns".
Installs into .claude/skills of the current project.
Are you the author of Swift Audit?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/rterakedis-swift-audit)
---
name: swift-audit
description: Audit planning docs, story files, and Swift source code against the Swift/SwiftUI guidance in docs/setup/swift/. Produces a remediation story file with one AC per finding. Use when the user says "swift audit", "audit swift", "audit codebase", or "check swift patterns".
---
# Swift Audit Skill
**Goal:** Scan the entire project — planning docs, story files, and Swift source code — against the guidance in `docs/setup/swift/`. Collate all findings into a single remediation story file that `/dev-story` can implement directly.
**Requires:** `docs/setup/swift/` must exist. If absent, tell the user to run `/setup` (for a new project) or `/refresh-swift` (to populate an existing project's swift docs).
---
## Step 1 — Load Guidance
Read all files present in `docs/setup/swift/`:
```bash
ls docs/setup/swift/
```
Read each file completely. These are the standards everything will be measured against. Pay particular attention to `anti-patterns.md` — it contains the hard rejection list used for code scanning.
---
## Step 2 — Inventory the Project
Run these commands to understand what exists:
```bash
# Planning docs
ls docs/prd.md docs/architecture.md docs/epics.md 2>/dev/null
# Story files
find docs/epics -name "*.md" | sort
# Swift source files (exclude build artifacts and dependencies)
find . -name "*.swift" \
! -path "*/DerivedData/*" \
! -path "*/.build/*" \
! -path "*/Pods/*" \
! -path "*/Packages/*" \
| sort
# Existing audit files (to avoid re-auditing completed work)
find docs/epics -name "swift-audit-*.md" | sort
```
Note what is present. If no Swift source files exist yet, skip Step 4 and note "no source to audit" in the report.
---
## Step 3 — Audit Planning Docs
Read each planning doc that exists and reason against the guidance. For each doc, look for:
### `docs/architecture.md`
| What to check | Rejection signal |
|---|---|
| Data layer / state management design | References MVVM, ViewModel classes, `ObservableObject`, `@Published` as the prescribed pattern |
| Dependency injection / service access | Prescribes init-parameter injection of services instead of `@Environment` |
| Concurrency / threading design | Prescribes `DispatchQueue`, `OperationQueue`, or Combine pipelines for new code |
| Project structure | Type-based structure (`/Models`, `/Views`, `/Services`) rather than feature-based |
| Navigation design | References `NavigationView` (deprecated) |
| Testing strategy | Prescribes XCTest classes rather than Swift Testing |
| iPadOS navigation (if applicable) | Prescribes `NavigationStack` alone as iPad root instead of `NavigationSplitView` |
| macOS settings (if applicable) | Prescribes a sheet or modal for app settings instead of `Settings` scene |
### `docs/prd.md`
| What to check | Rejection signal |
|---|---|
| Technical requirements or constraints | Mandates a specific architecture that conflicts with current guidance |
| Non-functional requirements | References threading or concurrency approaches that are now anti-patterns |
### `docs/epics.md`
| What to check | Rejection signal |
|---|---|
| Epic or story descriptions | Reference banned patterns in task or acceptance criteria language |
| Technical approach notes | Prescribe ViewModel files, Combine usage, or legacy observation in task descriptions |
### Story files (`docs/epics/*.md`, excluding `swift-audit-*.md`)
For each story file, check:
- Tasks that instruct creating `*ViewModel.swift` files
- Dev Notes that reference `ObservableObject`, `@Published`, `@StateObject`, `@ObservedObject`, `@EnvironmentObject`
- Acceptance Criteria that imply a banned pattern ("the ViewModel must expose…")
- Dev Notes that prescribe `DispatchQueue` or `import Combine`
---
## Step 4 — Audit Swift Source Code
Run grep commands to find hard violations in `.swift` files. For each match, record the file path and line number.
```bash
# Banned observation patterns
grep -rn "ObservableObject\|@Published\|@StateObject\|@ObservedObject\|@EnvironmentObject" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# DispatchQueue in non-legacy files
grep -rn "DispatchQueue" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Combine imports in new files
grep -rn "^import Combine" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# onAppear + Task leak pattern
grep -rn "onAppear" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" . \
| grep -v "// ✅"
# Deprecated NavigationView
grep -rn "NavigationView" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# ViewModel files
find . -name "*ViewModel.swift" \
! -path "*/DerivedData/*" \
! -path "*/.build/*" \
! -path "*/Pods/*" \
! -path "*/Packages/*"
# Array conversion of FetchedResults
grep -rn "Array(.*[Ff]etch" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# UIDevice idiom check (use size classes instead)
grep -rn "userInterfaceIdiom" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Screen-dimension layout math (breaks on iPhone Duo / Split View — use container bounds)
grep -rn "UIScreen.main\|UIScreen.screens" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Fixed-width frames (size relative to the container, not to device dimensions)
grep -rn "\.frame(width: [0-9]" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Hand-rolled bars — opt the app out of vertical bar presentation on iPhone Duo
grep -rn "UIToolbar(\|UINavigationBar(\|UITabBar(" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Index-based ForEach on mutable data
grep -rn "ForEach(0\.\.<\|ForEach(.*\.indices" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# Old TabView pattern (tabItem without Tab wrapper)
grep -rn "\.tabItem" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
# XCTest in new test files
grep -rn "^import XCTest\|XCTestCase" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
```
**Design token compliance (if `docs/ux/DESIGN.md` exists):** read its frontmatter token block, then grep for hardcoded colors that bypass it:
```bash
# Hardcoded colors in views (should come from the design token layer / asset catalog)
grep -rn "Color(red:\|Color(hex:\|Color(#colorLiteral\|UIColor(red:" \
--include="*.swift" \
--exclude-dir="{DerivedData,.build,Pods,Packages}" .
```
Flag each hit where a DESIGN.md token covers the value (MEDIUM). Values with no corresponding token are a DESIGN.md gap finding rather than a code finding.
For each `onAppear` hit, read surrounding lines (±5) to determine if it contains a `Task {` — only flag if it does. For `DispatchQueue` hits, read surrounding lines to determine if the file is a legacy wrapper (acceptable) or new code (flagged). Use judgment — do not flag things that are clearly intentional compatibility shims with a comment explaining why.
**Automation contract (only if `docs/setup/swift/testability.md` exists):**
```bash
# Interactive elements — compare against identifier count to size the gap
grep -rnE "Button\(|TextField\(|Toggle\(|Picker\(|NavigationLink\(|\.onTapGesture" \
--include="*.swift" --exclude-dir="{DerivedData,.build,Pods,Packages}" . | wc -l
grep -rn "accessibilityIdentifier" \
--include="*.swift" --exclude-dir="{DerivedData,.build,Pods,Packages}" . | wc -l
# Identifiers that break the {feature}-{element}-{role} kebab-case convention
grep -rhoE 'accessibilityIdentifier\("[^"]+"\)' \
--include="*.swift" --exclude-dir="{DerivedData,.build,Pods,Packages}" . \
| grep -vE 'accessibilityIdentifier\("[a-z0-9]+(-[a-z0-9\\()._]+)+"\)'
```
Flag non-kebab or label-shaped identifiers (`"Save Button"`, `"btn1"`) as MEDIUM `[CODE]` findings — they defeat the locator contract as surely as having none.
---
## Step 4b — Testability Foundation Check (brownfield retrofit)
Three greps decide whether this fires at all — zero cost when the foundation is present:
```bash
grep -rl "enum SeedScenario" --include="*.swift" . # seed registry
grep -rl '"--seed"\|"--uitest"' --include="*.swift" . # launch-arg contract
grep -rl '"--route"' --include="*.swift" . # unattended route delivery
xcodebuild -list -json 2>/dev/null | grep -i "UITests" # UI test target
grep -rl "onOpenURL" --include="*.swift" . # route table
```
If **all five** are present, record "testability foundation present" and skip to Step 5.
`--route` is checked separately from `--seed`/`--uitest` because a project can have a
complete `.onOpenURL` route table and still be undrivable: on iOS 26 an externally
opened custom-scheme URL raises a system confirmation alert, so unattended runs need
the route delivered as a launch argument. `onOpenURL` present + `"--route"` absent is a
real Stage 0 gap, not a formality — see `testability.md` (*Deep-Link Routes*).
If any are missing, the project cannot be driven deterministically — but do **not** emit one big-bang retrofit AC. Nobody runs a story that says "add identifiers to 60 views." Emit a **staged** remediation instead, written into the Step 6 story as separate task groups:
- **Stage 0 — Foundation (blocking).** Whatever of the five is missing: `SeedScenario` registry with `.empty`/`.typical`/`.edge`, the `--seed`/`--uitest`/`--reset` launch-argument contract with in-memory store isolation, a URL scheme + `.onOpenURL` route table **plus `--route` dispatching into that same table**, one UI test target with the `launch(seed:route:)` / `step(_:_:)` helpers, and 2–4 flows. This is the only stage that blocks; everything else builds on it. On a Core Data + CloudKit project, explicitly verify CloudKit is disabled on seeded/`--uitest` runs (see `testability.md`) — seeding into a CloudKit-backed store writes fixture rows to the developer's real private database.
- **Stages 1..N — Identifiers + routes, highest-traffic screens first.** One stage per screen or small screen group, each independently shippable. Rank screens by inbound-reference count — a deterministic zero-token traffic proxy:
```bash
# For each view file, how many places navigate to it
for f in $(find . -name "*View.swift" ! -path "*/DerivedData/*" ! -path "*/.build/*"); do
n=$(basename "$f" .swift)
printf "%s %s\n" "$(grep -rlE "NavigationLink|\.sheet|\.fullScreenCover|TabView" --include="*.swift" . \
| xargs grep -l "\b$n\b" 2>/dev/null | wc -l | tr -d ' ')" "$n"
done | sort -rn | head -20
```
Report the ranking in Dev Notes so the ordering is auditable rather than asserted. Each stage's AC: every interactive element and dynamic row on those screens gets a Design-Contract-style identifier, plus the screen's deep-link route. State the honest total in the report (`{k} of {total} screens covered by this remediation`) — never imply full coverage from a partial retrofit.
**Over-engineering / deletion pass:** scan touched source for complexity that shouldn't exist, one tagged line per finding — `delete:` (dead code / speculative feature), `stdlib:` (hand-rolled thing the standard library ships), `native:` (a dependency or code doing what the SDK already does — e.g. a custom formatter over `Foundation`), `yagni:` (an abstraction with one implementation, a protocol with one conformer, a config nobody sets), `shrink:` (same logic, fewer lines). Never flag a single smoke test / assert, validation, security, or accessibility for removal. These are `[CODE]` findings, usually LOW/MEDIUM.
---
## Step 5 — Triage Findings
Assign each finding a scope tag and severity:
**Scope tags:**
- `[DOC-ARCH]` — finding in `architecture.md`
- `[DOC-PRD]` — finding in `prd.md`
- `[DOC-EPICS]` — finding in `epics.md`
- `[STORY]` — finding in a story file
- `[CODE]` — finding in Swift source
- `[TESTABILITY]` — missing foundation / identifiers / routes (Step 4b). Stage 0 is HIGH; per-screen stages are MEDIUM.
**Severity:**
- `HIGH` — active anti-pattern that will produce wrong code when implemented (e.g., architecture prescribes `ObservableObject`; source file uses banned pattern in production path)
- `MEDIUM` — pattern that diverges from guidance but may not cause immediate breakage (e.g., `NavigationView` still compiles; XCTest still works)
- `LOW` — style or convention gap (e.g., type-based project structure that could be refactored; index-based `ForEach` on a list that currently never mutates)
Deduplicate: if the same file has five `@Published` properties, that is one finding with a note that multiple instances exist.
If zero findings across all scopes: skip Step 6, report "Clean — no violations found against current guidance." and stop.
---
## Step 6 — Write Remediation Story
Write to `docs/maintainer/swift-audit-{YYYY-MM-DD}.md`. Use today's date. Create `docs/maintainer/` if it does not exist.
### Story File Format
```markdown
---
Status: ready-for-dev
Type: remediation
Generated: {today's date}
---
# Swift Audit Remediation — {today's date}
**Source:** `/swift-audit` run against guidance in `docs/setup/swift/`
**Findings:** {N} total — {H} HIGH / {M} MEDIUM / {L} LOW
---
## Acceptance Criteria
<!-- DOC findings first, then CODE findings. HIGH severity first within each group. -->
### Planning Docs
- [ ] [HIGH][DOC-ARCH] {one-line description} — `docs/architecture.md`
- [ ] [MEDIUM][DOC-ARCH] {one-line description} — `docs/architecture.md`
- [ ] [MEDIUM][DOC-PRD] {one-line description} — `docs/prd.md`
### Story Files
- [ ] [MEDIUM][STORY] {one-line description} — `docs/epics/{filename}.md`
### Swift Source
- [ ] [HIGH][CODE] {one-line description} — `{File.swift}:{line}`
- [ ] [MEDIUM][CODE] {one-line description} — `{File.swift}:{line}`
---
## Tasks
### 1. Update Planning Docs
- [ ] Revise `architecture.md` to replace banned patterns with current guidance
- [ ] Revise `prd.md` where technical constraints conflict with current guidance
- [ ] Revise `epics.md` story/task language that references anti-patterns
### 2. Update Story Files
- [ ] Update story Dev Notes and task descriptions that prescribe banned patterns
### 3. Fix Swift Source
- [ ] Resolve all HIGH code findings before any MEDIUM/LOW
- [ ] Run the project's test suite after each batch of fixes to confirm no regressions
---
## Dev Notes
### Reference
Read all files in `docs/setup/swift/` before implementing any fix. The guidance files
are the source of truth for what each banned pattern should be replaced with.
### Finding Details
#### DOC-ARCH findings
{For each: what was found verbatim, file location, what it should say instead}
#### DOC-PRD findings
{same}
#### DOC-EPICS findings
{same}
#### STORY findings
{same — include story file name and the specific AC/task/Dev Note line}
#### CODE findings
{For each: file:line, the offending code snippet (1–3 lines), the replacement pattern from guidance}
### Notes
- Do not modify story files listed here if their Status is `in-progress` or `done` —
log a separate note in Completion Notes instead.
- For CODE findings in files not yet implemented (stubs or placeholders), update the
stub rather than writing the fix inline.
```
---
## Step 7 — Report to User
After writing the story file:
1. State the story file path.
2. Print the finding summary table:
| Scope | HIGH | MEDIUM | LOW | Total |
|---|---|---|---|---|
| DOC-ARCH | | | | |
| DOC-PRD | | | | |
| DOC-EPICS | | | | |
| STORY | | | | |
| CODE | | | | |
| TESTABILITY | | | | |
3. Say: "Run `/dev-story docs/maintainer/swift-audit-{date}.md` to implement all fixes."