| name | swift-code-cleanup |
| description | Measure and reduce technical debt in Swift / iOS code — Cognitive Complexity scoring, Clean Code audits (names, functions, types, error handling, tests), and concrete extract-refactor fixes for the Swift 6 / iOS 26 era. Use when the user asks to "clean up Swift code", "reduce cognitive complexity", "lower nesting", "untangle this function", "audit clean code", "refactor this ViewModel", "review naming / function design / error handling / tests", "split a massive view controller", "run a clean code review", or "find code smells" in a Swift codebase. |
| argument-hint | [file, directory, or pasted Swift code] |
| allowed-tools | ["Read","Bash","Edit","Glob","Grep"] |
Swift Code Cleanup
A single, opinionated cleanup toolkit for Swift / iOS code. It does two jobs:
- Measure — score Cognitive Complexity (G. Ann Campbell, SonarSource) and
audit against Clean Code (Robert Martin), reporting every smell with a line
number and severity.
- Reduce — apply concrete, mechanical refactors (extract function,
guard
flattening, struct-wrap arguments, named conditions, typed errors) and show a
before/after score.
The user provided: $ARGUMENTS
- A file path → read it and audit it.
- A directory → discover Swift files (skip build output) and audit each.
- Pasted code → audit inline.
- Nothing → ask which file/directory to clean, or to paste the code.
find "$ARGUMENTS" -name '*.swift' \
-not -path '*/.build/*' -not -path '*/build/*' \
-not -path '*/DerivedData/*' -not -path '*/Pods/*' \
-not -path '*/.swiftpm/*' -not -name '*.generated.swift'
Workflow
Run these phases in order. Always measure before you cut, and re-measure
after so the win is provable.
1. Scope & scan
Read the target file(s). For each function, method, computed property, and
non-trivial closure, compute its Cognitive Complexity score (algorithm below).
Note Clean Code violations in parallel (the audit checklist lives in
references/clean-code-audit.md).
2. Report
Lead with a per-file table of every function scoring ≥ 5, sorted worst-first,
plus a severity-bucketed list of Clean Code findings. Use the exact report
format in the "Report format" section.
3. Prioritise
Fix in this order — highest leverage first:
- Correctness smells — force unwraps on external data, silent
catch {},
nil used to signal an error. These are latent crashes/bugs.
- Cognitive Complexity ≥ 25 — refactor (extract, flatten, name).
- Cognitive Complexity 16–24 — review and reduce where cheap.
- SRP / God-object splits — propose a plan, get confirmation, then act.
- Naming, comment noise, formatting — apply directly.
4. Fix
Apply mechanical fixes with Edit. The five highest-yield moves (each one
lowers the score — see references/refactoring-recipes.md):
- Extract a deeply-nested closure/loop body into a named function.
- Replace a nested
if let chain with stacked guard let … else { return }.
- Hoist a complex boolean into a named computed property / function.
- Flatten callback pyramids into
async/await.
- Move
switch-case bodies into one function per case.
For structural changes (split a class, introduce a protocol seam), describe
the plan and ask before modifying.
5. Re-measure & summarise
Recompute scores and show before → after for every function you touched. End
with a one-line tally (functions touched, total score delta, issues fixed).
Cognitive Complexity — scoring algorithm
Cognitive Complexity measures mental effort to understand code, not path
count (that is Cyclomatic Complexity). Flat code is cheap; deep nesting is
expensive even at the same branch count. Three rule groups:
A — Structural increments (+1 each, plus the current nesting level)
| Swift construct | Increment |
|---|
if, else if | +1 (+ nesting) |
guard … else | +1 (+ nesting) |
switch | +1 (+ nesting) |
for, while, repeat…while | +1 (+ nesting) |
catch | +1 (+ nesting) |
Ternary ? : | +1 (+ nesting) |
| Nested / trailing closure with a body | +1 (+ nesting) |
| Recursive call (direct or mutual) | +1 (flat) |
else (the bare fall-through of an if/else) adds +1 with no nesting
penalty — it is a continuation, not a new decision.
B — Nesting penalty
Each structural increment that sits inside another structural scope adds +1
per level of nesting. A closure is itself an extra level.
func f() {
if a {
for b in c {
if d {
}
}
}
}
items.forEach { item in
if item.isValid {
item.process()
}
}
C — Flat increments (+1, no nesting penalty)
| Swift construct | Increment |
|---|
break / continue to a label | +1 |
| Each contiguous run of one boolean operator in a mixed sequence | +1 per run |
Mixed boolean operators: count each maximal run of the same operator as one
increment; switching && ↔ || starts a new run.
if a && b && c { }
if a && b || c { }
if a && b || c && d || e { }
Thresholds (SonarQube defaults)
| Score | Verdict |
|---|
| 0–15 | ✅ Good |
| 16–24 | ⚠️ Review |
| 25+ | ⛔ Refactor |
Full worked examples, the nested-closure / guard-stacking / switch patterns, and
the Cognitive-vs-Cyclomatic contrast table are in
references/cognitive-complexity.md.
Clean Code audit — what to flag
For each file, walk these dimensions. The complete checklist (with Swift
examples for every item) is in references/clean-code-audit.md; the short list
of the most damaging smells:
- Names — cryptic/abbreviated names, noise suffixes (
Data/Info/Manager),
booleans not phrased as assertions, magic numbers without a named constant.
- Functions — over ~20 lines, doing more than one thing, mixing abstraction
levels, 3+ parameters,
Bool flag parameters, hidden side effects, duplicated
blocks (DRY).
- Types — SRP violations / God objects, classes used for pure data (prefer
struct), singletons instead of injected dependencies, logic smeared across
many extensions, massive view controllers.
- Error handling —
nil returned to signal failure (use throws/Result),
NSError/String as errors instead of a typed enum: Error, silent
catch {}, force unwrap on external input, careless try?.
- Tests — FIRST violations (real network/DB/clock in unit tests), multiple
unrelated assertions per test, vague test names, testing private methods.
Severity scale (use these exact buckets in the report)
- Critical — crashes / silent data loss: force unwrap on external data,
swallowed errors,
nil-as-error.
- Major — SRP violations, functions > 50 lines, missing error context,
duplicated logic, untestable concrete dependencies.
- Minor — naming, comment noise, formatting, organisation.
Modern Swift defaults (resolve every fix toward these)
This is the Swift 6 / iOS 26 era — when an old source disagrees, pick the
modern form:
- Concurrency over callbacks.
async/await and structured concurrency
flatten nesting (a 10-point pyramid becomes 1). Don't add (T?, Error?)
completion handlers to new code. Respect Swift 6 strict concurrency: actor
isolation and Sendable are part of "clean".
- Typed throws where the error set is closed. Swift 6 allows
func parse() throws(ParseError) -> Config. Use it when the caller benefits
from an exhaustive error type; keep untyped throws for open-ended failures.
- Swift Testing for new tests. Prefer
@Test / #expect / @Suite
(import Testing) over XCTest in new files; both can coexist. Use
#expect(throws:) for error assertions and parameterised @Test(arguments:)
to kill duplicated test bodies.
guard let x over if let x for the happy path / early exit. Swift's
shorthand guard let x else { return } (no = x) is the current idiom.
- Value types first.
struct unless identity, inheritance, or deinit is
genuinely required.
- Protocol seams for testability. Depend on a protocol and inject it via
init; never test against concrete URLSession / CoreData / FileManager.
Lint & format tooling (delegate the mechanical, focus on the cognitive)
A cleanup pass should run the deterministic linters first so you spend your
effort on what tools can't see (complexity, SRP, naming intent). If the repo
has configs, run them; otherwise suggest adding them. Quick probe:
ls .swiftlint.yml .swift-format swiftlint.yml .swiftformat 2>/dev/null
command -v swiftlint swift-format 2>/dev/null
- SwiftLint (
swiftlint lint --quiet) — the cyclomatic_complexity,
function_body_length, type_body_length, large_tuple, force_unwrapping,
and nesting rules catch much of the Clean Code surface mechanically. Its
--fix (autocorrect) handles trivial style. Adopt it via the SwiftLintPlugins
build-tool plugin + a baseline; never --fix in a build phase.
- swift-format (
swift-format lint -r Sources/, swift-format -i -r) —
Apple's formatter; the in-place mode applies whitespace/brace conventions so
you never hand-edit formatting.
- Periphery (
periphery scan) — the only tool that finds unused
declarations across the whole module graph, including dead public API
(needs a build; --retain-public for libraries). A per-file linter cannot.
- Dependency-free scanners in
references/scripts/ for when no tooling is
installed: quality-gates.sh (build/tests/new-force-unwraps/staged-junk
gates), detect-swiftui-antipatterns.sh (SwiftUI-semantic smells SwiftLint
has no rules for), check-doc-snippets.sh (compile-check doc code).
Tool output is the floor; this skill provides the ceiling — Cognitive
Complexity scoring and structural refactors that no linter performs. Detailed
config snippets, the SwiftLint+CI adoption playbook, the Periphery scan, and the
exact rule mapping are in references/lint-tooling.md.
Report format
Emit exactly this shape (one block per file):
## CheckoutViewModel.swift
| Function | Cognitive Complexity | Verdict |
|---|---|---|
| processPayment() | 28 | ⛔ Refactor |
| validateInput() | 18 | ⚠️ Review |
| formatTotal() | 4 | ✅ Good |
### processPayment() — Score 28
Breakdown:
- L12 `if status == .pending` → +1 (level 0)
- L14 `for item in cart` → +2 (level 1)
- L16 `if item.isValid` → +3 (level 2)
- L19 `a && b || c` → +2 (mixed operators)
- L23 nested completion closure → +3 (level 2 closure)
...
Suggested refactoring:
- Extract the cart-validation loop (L14–L21) into `validate(_:) -> [LineItem]`.
- Hoist `status == .pending && !cart.isEmpty` into `var isReadyToCharge: Bool`.
- Replace the completion-closure pyramid with `async`/`await`.
### Clean Code findings — CheckoutViewModel.swift
Critical:
- L31 `response.user!` — force unwrap on API data → `guard let user = response.user else { throw … }`
Major:
- ViewModel owns networking + parsing + persistence → split out `CheckoutRepository`
Minor:
- L8 `mgr` → `paymentGateway` (intention-revealing name)
Finish with the tally:
## Cleanup summary
- Files reviewed: N
- Functions refactored: N (total Cognitive Complexity −Δ)
- Critical fixed: N · Major fixed: N · Minor fixed: N
- Needs manual structural work: [list]
Whole-app audit (beyond one file at a time)
For "audit the whole app / find all the tech debt / comprehensive review"
requests, escalate from the per-file pass to the split-by-dimension audit in
references/audit-orchestration.md: scope + hot-spots → capture compiler
warnings as ground truth → fan out three parallel Explore agents (concurrency &
API modernity / dead code & duplication / bugs, logic, security, performance) →
SwiftUI specialist pass after → verify every Critical at the cited line →
synthesize one numbered CODE_AUDIT.md. The copy-paste agent briefs +
placeholder-discovery table live in references/audit-agent-briefs.md; the
security agent delegates MASVS depth to ios-security/references/audit-workflow.md.
The audit-honesty rules there (verify before propagating, demote if
unreproducible, one finding per root cause, mandatory Verification section) and
the stable ## N. / ### N.M report numbering apply to any multi-finding
deliverable.
References
- references/cognitive-complexity.md — full scoring rules, worked examples,
the four high-complexity Swift patterns (nested closures, guard stacking,
boolean conditions, switch side-effects), Cognitive vs Cyclomatic table.
- references/clean-code-audit.md — the complete per-dimension checklist
(names · functions · types · errors · tests · comments · formatting) with a
Swift example for every rule, plus the Agent Smell AI-generated-code
checklist and the DocC documentation policy.
- references/refactoring-recipes.md — the mechanical extract/flatten/name
recipes, each shown as a before/after with the score delta.
- references/lint-tooling.md — SwiftLint + swift-format + Periphery config and
the rule-to-smell mapping; the SwiftLint+CI adoption playbook; the three
dependency-free scanners; what to automate vs what to hand-review.
- references/audit-orchestration.md — whole-app audit pipeline, audit-honesty
rules, the numbered
CODE_AUDIT.md skeleton.
- references/audit-agent-briefs.md — the three copy-paste Explore-agent briefs
and the placeholder-discovery table.
- references/metaprogramming.md — authoring Swift macros, custom
@resultBuilder DSLs, and SwiftPM build-tool / command plugins (for deleting
boilerplate that's become tech debt). Niche; not core to a cleanup pass.
references/scripts/ — quality-gates.sh, detect-swiftui-antipatterns.sh,
check-doc-snippets.sh (dependency-free, read-only).