Debugging Apple-platform issues: crashes, leaks, hangs and weird behavior — read the artifact, reproduce, prove the cause; Socratic only when the user is learning. Trigger: load on crashes, memory leaks, hangs, "it behaves weird", or any "why is this happening?" in Swift/iOS/macOS.
Installs into .claude/skills of the current project.
Sponsors
SSkills DirectorySkills Directory
Know which skills are safe — weekly.
SSkills DirectorySkills Directory
Is that skill safe to install?
Are you the author of Lh Debugging?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/jrodrigopuca-lh-debugging)
---
name: lh-debugging
description: >
Debugging Apple-platform issues: crashes, leaks, hangs and weird behavior —
read the artifact, reproduce, prove the cause; Socratic only when the user
is learning.
Trigger: load on crashes, memory leaks, hangs, "it behaves weird", or any
"why is this happening?" in Swift/iOS/macOS.
license: Apache-2.0
metadata:
author: jrodrigopuca
version: "1.1"
local-agents-id: "sk-51c2b2"
---
_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 relying on it._
## Critical Patterns
### 1. Method first; Socratic only on the learning signal
Read the artifact, reproduce, prove the cause, then fix. When the user is
learning ([judgment #6](../../AGENTS.md)), walk the same steps as questions
("what EXACTLY does the crash log say?", "what's the last thing you know is
true?"), because debugging skill compounds for life; tooling errors (code
signing, DerivedData corruption, simulator weirdness) get the answer straight
either way.
### 2. Read the crash before theorizing
Every crash diagnosis starts with the artifact, not the vibes:
| Crash says | It usually means |
|------------|------------------|
| `EXC_BAD_ACCESS` | Accessing freed memory — over-released object, dangling pointer, race |
| `Fatal error: Unexpectedly found nil` | A force unwrap met reality — read WHICH line, ask why the invariant broke |
| `EXC_BREAKPOINT` on a Swift runtime line | Failed precondition: array index, force cast, arithmetic |
| Main-thread checker / purple warnings | UI touched off main — actor isolation hole |
| Watchdog kill (`0x8badf00d`) | Main thread blocked too long at launch/foreground |
| `EXC_CRASH (SIGABRT)` + `NSException` ("unrecognized selector", `NSInvalidArgumentException`) | ObjC-side throw: bridging, KVC key, a selector that isn't there — read the exception REASON line, not the frame |
| `dispatch_assert_queue` / "Incorrect actor executor assumption" | Swift 6 isolation check: `@MainActor` code entered from a non-main thread through a C/ObjC callback — the CALLER is wrong, not the annotation |
Symbolicate first if it's from the field; then find the LAST frame that's
YOUR code. The stack tells you where it died — the question to ask is
"where did the bad state get CREATED?", which is earlier.
### 3. The reproduction discipline (inherited, domain-tuned)
Same law as [qa's `lh-bug-reporting`](../../../qa/skills/lh-bug-reporting/SKILL.md) and the
[generalist's `lh-verification`](../../../generalist/skills/lh-verification/SKILL.md): reproduce
before fixing, one hypothesis at a time, binary-search the surface (comment
out half the view, mock the service, hardcode the input). Apple-specific
multipliers to check early because they explain "sometimes": simulator vs
device, debug vs release (optimization changes timing), first-install vs
upgrade (migration state — for SwiftData the store from the OLD build is the
repro, a fresh install proves nothing), airplane mode, low-power mode,
different OS versions.
### 4. Memory leaks: prove the graph
Symptoms: memory climbing screen after screen, `deinit` never printing,
doubled network responses (two live instances subscribed). Method: add a
`deinit { print("...") }` to the suspect (the poor man's leak detector), then Instruments' Leaks/Allocations or Xcode's Memory Graph
Debugger to SEE the retain cycle: who holds whom. The usual suspects are the
[lh-code-review 🔴 list](../lh-code-review/SKILL.md): strong `self` in stored
closures, strong delegates, long-lived `Task`s. Fix = ownership decision
(`weak`), not a random sprinkle of weakness until the symptom hides.
### 5. Hangs and jank: measure, then blame
Main-thread stalls have three usual causes: synchronous I/O on main (disk,
network, big decode), massive view recomputation (a `body` doing work — see
[lh-state-architecture](../lh-state-architecture/SKILL.md)), or layout thrash.
Don't guess between them: the Hangs instrument (and Organizer's hang
reports from the field) says WHEN the main thread stalled, then Time
Profiler says where that time went ("performance is measured, not guessed"
— the [senior-dev rule](../../../senior-dev/AGENTS.md), same religion).
Read the heaviest stack trace before touching any code.
### 6. Weird SwiftUI behavior: it's almost always identity or ownership
View not updating → who owns the state, is mutation reaching the owner, is
the model actually `@Observable`? Updating too much → observation too
coarse, body too big. State resetting mysteriously → view IDENTITY changed
(position in the tree, `.id()`, conditional branches) so SwiftUI built a new
view with fresh `@State`. Animation glitches → same identity story. The
mental model — SwiftUI diffs a tree of descriptions; identity decides
what survives — and these bugs become predictable instead of spooky. First
tool, before theory: `Self._printChanges()` in `body` names the property
that triggered the re-evaluation, and Xcode 26's SwiftUI instrument shows
the update's cause and effect across the tree.
## Resources
- Sibling skills: [lh-code-review](../lh-code-review/SKILL.md) (the defect
patterns), [lh-state-architecture](../lh-state-architecture/SKILL.md) (the
ownership model these bugs violate)
- The debugging epistemics this domain-tunes:
[generalist/lh-verification](../../../generalist/skills/lh-verification/SKILL.md),
[senior-dev/lh-pairing](../../../senior-dev/skills/lh-pairing/SKILL.md) (co-debugging protocol)