lh-code-review · v1.1 · 2026-10-14 · sha256 c7a04205ccbd01da
lh-code-review v1.1A
Immutable. This exact content never changes and is served at /api/v1/blob/c7a04205ccbd01da.
---
name: lh-code-review
description: >
Reviewing Swift/SwiftUI code as mentor: the severity system, the
Apple-specific defect checklist, and review-as-teaching. Trigger: load when
the user shares Swift, SwiftUI, UIKit, or AppKit code for review, feedback,
or "is this okay?".
license: Apache-2.0
metadata:
author: jrodrigopuca
version: "1.1"
local-agents-id: "sk-1c68f0"
---
_Verified against Xcode 26 / iOS 26 / macOS 26 (September 2026). Apple moves
these facts yearly — if the year has changed, re-check a claim before teaching it._
## Critical Patterns
### 1. Review in three passes, report by severity
Pass 1 — **correctness & safety** (🔴): retain cycles, force unwraps, data
races, main-thread violations, leaked resources. Pass 2 — **design** (🟡):
logic in views, missing abstractions, state ownership, testability. Pass 3 —
**style** (🟢): naming, idiom, formatting. Report blockers first; never bury
a retain cycle under twelve naming nits. Every finding carries its failure
case ("this closure captures self strongly; when X outlives Y, the whole
graph leaks") — and when the code is genuinely good, say WHAT is good with
the same precision.
### 2. The Swift-specific 🔴 checklist
- **Retain cycles**: `self` captured strongly in escaping closures stored by
`self` (handlers, subscriptions, timers); delegates declared strong. A
bare `Task { }` retains `self` only until it finishes — that is not a
cycle; the leak needs BOTH `self` storing the task AND the task never
completing (an endless `for await`). `[weak self]` sprinkled on every Task
is cargo cult: prove the ownership graph, and cancel stored tasks on scope
exit instead.
- **Force unwraps** (`!`, `try!`, `as!`): each one is a crash with a date TBD.
Acceptable only with a written invariant ("IBOutlet after viewDidLoad") —
otherwise `guard let` with a real fallback path.
- **Concurrency**: UI state touched off the main actor; shared mutable state
without actor/lock protection; `Task` fire-and-forget hiding errors;
blocking calls inside actors. In Swift 6 language mode, isolation and
Sendable violations are compile errors; blocking inside an actor and
fire-and-forget Tasks are NOT — the compiler is an ally for the first two
only. And under Xcode 26's MainActor default with caller-actor async, the
new 🔴 runs the other way: heavy work silently on main because nobody
marked it `@concurrent` (see [lh-state-architecture #6](../lh-state-architecture/SKILL.md)).
- **Error swallowing**: `try?` discarding errors the user needs to see;
empty catch blocks — an error that vanishes silently becomes QA's
[lh-flow-hunting](../../../qa/skills/lh-flow-hunting/SKILL.md) finding later.
### 3. The SwiftUI 🟡 checklist
- **Logic in the view**: formatting, filtering, network calls inline in
`body` → belongs in the ViewModel/model; `body` is a pure function of
state, and every recomputation runs it.
- **State ownership**: `@State` for view-local only; one source of truth per
piece of state — two views each "owning" a copy is the bug factory;
`@Binding` down, not state duplicated down.
- **Observation hygiene**: with `@Observable`, a view re-evaluates only when
a property its `body` READ changes — so "re-renders on unrelated changes"
is the `ObservableObject`/`@Published` smell (any published change
invalidates every observer), and `ObservableObject` in new code is the
finding. For `@Observable` models the finding is a `body` that reads far
more than it shows: narrow what `body` touches or split into subviews
(diffing works per view).
- **SwiftData**: `ModelContext` or `@Model` objects crossing actors 🔴 (not
Sendable); `@Query` inside a ViewModel 🟡 (view-only, main-actor);
relationships without delete rules 🟡; a schema change in a shipped app
without a `VersionedSchema` + migration plan 🔴 — that is a crash on
upgrade, not on first install.
- **Untested-by-omission** 🟡: a ViewModel behind a protocol-fronted service
with no Swift Testing target is testability that never cashed in; and a
`@MainActor` ViewModel's tests are `@MainActor`/awaited or they don't
compile in Swift 6.
- **The missing states**: view renders happy path only — where are loading,
empty, error? (The screen-state enum from
[lh-state-architecture](../lh-state-architecture/SKILL.md), and the ux-ui
[five states (`lh-ux-flows`)](../../../ux-ui/skills/lh-ux-flows/SKILL.md) — same table.)
### 4. Platform-honesty: iOS code isn't Mac code automatically
When the target includes macOS: check window/scene assumptions (Mac has many
windows, resizable), keyboard/menu/right-click support, `NavigationSplitView`
over stack navigation for wide layouts, sandbox entitlements for file access.
Flag "iOS-shaped" code shipped to Mac as 🟡 — it runs, and it feels like a
port, which Mac users smell instantly.
### 5. Review as teaching: pattern over instance
The user's growth beats the diff. When the same mistake appears three times,
teach the PATTERN once (with the underlying rule: "closures that outlive the
call need an ownership decision") instead of flagging three instances — then
have them find the third occurrence themselves. One concept TAUGHT per review
(mentorship contract #7); the rest goes to pending topics. That limits the
teaching, never the report: every blocker is named in this review, whatever
the count — a crash moved to pending topics is a crash shipped.
## Resources
- Sibling skills: [lh-state-architecture](../lh-state-architecture/SKILL.md) (where
the design findings point), [lh-debugging](../lh-debugging/SKILL.md) (when a
finding needs a repro)
- Generic review altitude and severity honesty:
[senior-dev/lh-pairing](../../../senior-dev/skills/lh-pairing/SKILL.md),
[architect/lh-design-review](../../../architect/skills/lh-design-review/SKILL.md)