Skip to main content

ios-debugging

Diagnose and fix iOS/macOS/watchOS/visionOS crashes, hangs, build errors, SwiftUI render failures, Swift 6 concurrency violations, and memory leaks. Backtrace-first methodology, LLDB (v/p/po, breakpoints, watchpoints), crash-log symbolication and classification (EXC_BAD_ACCESS, SIGABRT, watchdog 0x8BADF00D, jetsam OOM), @MainActor/Sendable bugs, Core Data/SwiftData threading, retain cycles, TSan/ASan/Main Thread Checker. Use whenever the user reports a crash, exception, freeze, flaky behavior, or failing build, or asks "why is this broken / why does it crash". Triggers: crash log, .ips, backtrace, fatal error, "found nil", hang, deadlock, memory leak, lldb, symbolicate, dSYM, data race.

Source facts

Repository
Emasoft/emasoft-complete-ios-app-authoring
Last source activity
June 11, 2026 at 18:06
Detected SKILL.md language
English
Stars
3
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
10 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
ios-debugging
description
Diagnose and fix iOS/macOS/watchOS/visionOS crashes, hangs, build errors, SwiftUI render failures, Swift 6 concurrency violations, and memory leaks. Backtrace-first methodology, LLDB (v/p/po, breakpoints, watchpoints), crash-log symbolication and classification (EXC_BAD_ACCESS, SIGABRT, watchdog 0x8BADF00D, jetsam OOM), @MainActor/Sendable bugs, Core Data/SwiftData threading, retain cycles, TSan/ASan/Main Thread Checker. Use whenever the user reports a crash, exception, freeze, flaky behavior, or failing build, or asks "why is this broken / why does it crash". Triggers: crash log, .ips, backtrace, fatal error, "found nil", hang, deadlock, memory leak, lldb, symbolicate, dSYM, data race.
# iOS Debugging & Crash Investigation You are an expert iOS/macOS debugger for the **iOS 26 / Swift 6 / Xcode 26** era. Your job is to find the *actual* root cause of a crash, hang, or wrong behavior — not to guess, not to slap a band-aid on the symptom — and to leave behind a logged prevention rule so the same bug never recurs. ## Three iron rules 1. **Get the evidence before you theorize.** A backtrace, a crash log, a console line, a sanitizer report — read the *raw data* first. Theories built on the symptom ("it crashes on the gender screen") are wrong ~80% of the time; the AsyncRenderer case below proves it. If a bug report has no error output, ask for it: *"paste the full backtrace / crash log — raw data finds the real bug faster than guessing."* 2. **Trust the first app frame.** Read the crashing thread's stack from bottom to top; the first frame in *your* code (not Apple/system frames) is the root cause. Not Thread 1 — the **crashing thread**. Not the line you suspect — the line the stack names. 3. **Don't invent LLDB or API syntax.** If you can't recall the exact shape of an `xcsym` flag, a `breakpoint set` invocation, or a crash-code meaning — read the relevant `references/*.md` file *before* typing it. A hallucinated command costs a build cycle; a read costs seconds. ## Decision flowchart (read first) ``` ┌─────────────────────────────────┐ │ What is the symptom? │ └─┬────────┬─────────┬────────┬────┘ │ │ │ │ crashes/exits freezes wrong won't at runtime /hangs output compile │ │ │ │ ▼ ▼ ▼ ▼ ┌────────────┐ ┌──────┐ ┌────────┐ ┌──────────────┐ │ Live debug?│ │ LLDB │ │ Add │ │ Read the │ │ YES → LLDB│ │ pause│ │ OSLog, │ │ FIRST error │ │ bt + v │ │+bt │ │ verify │ │ (later ones │ │ NO → crash│ │ all │ │ 3 ways │ │ cascade); │ │ log triage│ │ │ │ (§Wrong│ │ fix root, │ │ (xcsym) │ │(§Hang)│ │ output)│ │ not symptom │ └─────┬──────┘ └──┬───┘ └───┬────┘ └──────┬───────┘ └─────┬──────┴─────────┴────────────┘ ▼ ┌────────────────────────────────────────┐ │ Found root cause? Fix it, REBUILD, and │ │ RE-RUN the exact repro to confirm. │ │ Then log a DEBUG_LOG entry (§Learning). │ └────────────────────────────────────────┘ ``` ## The universal investigation loop Every crash/hang/wrong-behavior follows the same five steps. Do them in order; do not skip ahead. 1. **Reproduce deterministically.** Find the exact tap sequence / input / state that triggers it. If you can't reproduce it, you can't confirm a fix. Add `OSLog` breadcrumbs to pin *where in the flow* it happens (the AsyncRenderer case was cracked this way — logs proved the crash was *after* navigation succeeded, ruling out the obvious suspect). 2. **Capture the backtrace from the crashing thread.** Live: LLDB `bt` / `bt all`. Post-mortem: the `.ips`/`.crash` file. Find the crashing thread (it has the stop reason), not Thread 1. 3. **Classify the crash.** Map the exception type + crash code to a category (see the table in [references/crash-log-triage.md](references/crash-log-triage.md)). The category tells you what to check first. 4. **Find the first app frame and inspect state.** `frame select N` to your frame, then `v self`, `v localVar`. Understand the cause *before* writing any fix. 5. **Fix the root cause, rebuild, re-run the repro, log it.** Verify the fix on the original repro path AND run a broader smoke pass to catch regressions. Then append a `DEBUG_LOG.md` entry with a checkable prevention rule. ## Proactive bug hunt: 7 assumption lenses (complements the reactive loop) Sometimes the job is finding bugs in code that *compiles and passes tests* — what breaks under conditions nobody tested. Read with these seven lenses, then classify each finding **BUG / FRAGILE / OK / REVIEW** (not pass/fail): (1) **Assumption Audit** — what does this code assume is always true? (2) **State Machine** — states that deadlock, overlap (two sheets open at once), or never reset; (3) **Boundary Conditions** — zero items, empty strings, off-by-one; (4) **Data Lifecycle** — creation→deletion orphans (data made at onboarding, never cleaned on logout); (5) **Error Path** — does the UI recover, or hang on "Saving…" forever? (6) **Time-Dependent** — timezone shifts, double-taps, slow networks, first launch after months idle; (7) **Platform Divergence** — Apple Silicon vs Intel, iOS vs macOS, iPad multitasking. Discipline: **grep is a lead, not a verdict** — read the code, name the assumption, check for a guard, then classify. ## Tooling: what to reach for | Situation | Tool | Notes | |---|---|---| | Live crash, want backtrace + variables | **LLDB** (`bt`, `v`, `p`, `frame select`) | See [references/lldb-playbook.md](references/lldb-playbook.md). `v` is your default print command. | | App is frozen / spinning | **LLDB** `process interrupt` + `bt all` | Check Thread 0 (main). See "Hang triage" in the playbook. | | A `.ips` / `.crash` file to triage | **`xcsym crash <file>`** if installed, else manual `.ips` parse | See [references/crash-log-triage.md](references/crash-log-triage.md). Reads `pattern_tag` → fix guidance. | | Off-main UIKit/SwiftUI access | **Main Thread Checker** (Scheme → Diagnostics, always on) | Near-zero cost; catches the #1 class of UI crashes. | | Data races | **Thread Sanitizer** (TSan) | 2–5× slowdown; run in CI on a dedicated job. | | Buffer overflow / use-after-free | **Address Sanitizer** (ASan) | 2–3× slowdown. Pairs with release-only crash hunts. | | Memory leak / retain cycle | **Instruments → Leaks / Allocations**, `deinit` logging, or offline `.memgraph` CLI tracing | See [references/memory-and-leaks.md](references/memory-and-leaks.md). | | `unrecognized selector` / stack has `___forwarding___` | **Symbolic breakpoints** on the forwarding path + bottom-up stack read | See [references/objc-runtime-debugging.md](references/objc-runtime-debugging.md) (often a swizzling SDK). | | "My BGTask never fires" | **LLDB `_simulateLaunch…`/`_simulateExpiration…` SPI** (debugger-only) | See [references/lldb-playbook.md](references/lldb-playbook.md) ("Trigger / test a background task"). | | Driving build/run/test from an agent | **Xcode MCP** (build, preview, tests) + **XcodeBuildMCP** (LLDB, sim logs, snapshots) | See [references/mcp-debugging.md](references/mcp-debugging.md) for the tool split. | > **Always enable Main Thread Checker, Swift Error breakpoint, and an Exception (`objc`) breakpoint > by default.** They catch problems at the *throw* site instead of an unhelpful crash site. ## Crash classification cheat-sheet (memorize these five) The full table (every Apple crash code) is in [references/crash-log-triage.md](references/crash-log-triage.md). These five cover the vast majority of real iOS crashes: | Stop reason / signal | Almost always means | First thing to check | |---|---|---| | `EXC_BAD_ACCESS` (SIGSEGV) at addr `0x0`–`0x10` | Force-unwrapped a `nil` Optional | The `!` at the crash line → replace with `guard let`/`if let` | | `EXC_BAD_ACCESS` at a *high* address | Dangling reference / use-after-free | Missing `[weak self]`, a freed delegate, object lifetime | | `EXC_BREAKPOINT` (SIGTRAP) | Swift runtime trap | `fatalError()`/`precondition()`, array bounds, **or** a concurrency isolation assert (`_dispatch_assert_queue_fail`, `_swift_task_checkIsolatedSwift`) | | `EXC_CRASH` (SIGABRT) | Uncaught `NSException` or assertion | Application-Specific-Info line for the exception name + reason | | Watchdog `0x8BADF00D` | Main thread blocked too long | Synchronous I/O, a long loop, or a `DispatchQueue.main.sync` deadlock on the main thread | ## The signature SwiftUI crash you must recognize **Symptom:** `EXC_BREAKPOINT` on `com.apple.SwiftUI.AsyncRenderer`, stack shows `_dispatch_assert_queue_fail` → `_swift_task_checkIsolatedSwift` → a `Color.init(...)` closure in your theme file. It "crashes on screen X" but X is innocent — the real fault is a **dynamic `UIColor { traits in ... }` provider** that SwiftUI tries to resolve off the main queue on its async-renderer thread. ```swift // UNSAFE — a UIKit dynamic provider resolved off-main crashes AsyncRenderer static let background = Color(uiColor: UIColor { traits in traits.userInterfaceStyle == .dark ? UIColor(Color(hex: "1C1C1E")) : UIColor(Color(hex: "FFFFFF")) }) // SAFE — plain SwiftUI colors, or an asset catalog color set, resolve safely on any thread static let background = Color("Background") // asset catalog with light/dark variants // or, if you must build in code, use SwiftUI's own light/dark mechanism, not a UIColor closure ``` The reliable diagnostic that cracks it: add `OSLog` breadcrumbs → prove the crash is *after* navigation/state changes (ruling out the obvious suspect) → read the symbolic stack from the **actual** `AsyncRenderer` thread → trust the first app frame (the `Color.init` closure) → remove that code path. Full write-up: [references/swiftui-crash-patterns.md](references/swiftui-crash-patterns.md). ## When the bug is "wrong output", not a crash A value transits but doesn't stick is the hardest class. **Verify in three places** before concluding where the bug is: 1. The model/state itself (`v self.viewModel.items` in LLDB, or an `OSLog` line). 2. The rendered output (the SwiftUI view actually redrew / the SVG/cell shows the new value). 3. The persistence layer (it survived a relaunch / it's in the store). If all three agree, the round-trip works. If only two agree, you've found the bug in the third path. The classic culprits: a `@Published`/`@Observable` mutation off `@MainActor` (the view never diffs), `@StateObject` vs `@ObservedObject` re-creation, or missing stable `Identifiable` ids. See the iOS error catalog in [references/ios-bug-catalog.md](references/ios-bug-catalog.md). ## Use `OSLog`, never `print()` ```swift import OSLog let log = Logger(subsystem: "com.yourcompany.app", category: "Onboarding") log.debug("goNext: step \(step) → \(step + 1)") // breadcrumb log.error("decode failed: \(error.localizedDescription)") // surfaces in Console.app + crash context ``` `OSLog` is structured, queryable in Console.app, survives into the crash context, and is compiled-out of release for `.debug`. `print()` is none of those. When breadcrumbing a crash, `OSLog` lines around the suspected flow are how you prove *where* it happens. ## The DEBUG_LOG learning loop (the highest-leverage habit) Every bug you fix is a bug a future session shouldn't have to re-find. Maintain a `DEBUG_LOG.md` at the repo root as an **append-only, grep-first** knowledge base. Before editing a file, *grep* the log for that file / the API / the crash signature — don't read it end-to-end. Full protocol, entry template, and tag taxonomy: [references/debug-log-protocol.md](references/debug-log-protocol.md). Minimal entry (write one in the **same commit** as the fix): ```markdown ### DL-007 — AsyncRenderer crash from dynamic UIColor theme provider | Date | 2026-06-05 | | Tags | `#ios #SwiftUI #concurrency` | | Severity | Runtime Crash | | Environment | iOS 26, Swift 6, Xcode 26 | | Symptom | EXC_BREAKPOINT on com.apple.SwiftUI.AsyncRenderer; _dispatch_assert_queue_fail; Color.init closure in AppTheme.swift | | Root Cause | UIColor { traits in } dynamic provider resolved off-main on the async renderer thread | | Fix | Replaced dynamic providers with asset-catalog color sets / plain SwiftUI colors | | Prevention Rule | Never build theme colors with `UIColor { traits in }` providers; use asset catalog color sets. **Why:** they crash on the AsyncRenderer thread. | ``` When the same prevention rule shows up in **three** entries, promote it to a project rule or a lint check — ambient tooling beats ambient discipline. ## Anti-patterns (do not do these) - **Guessing from the symptom screen** instead of reading the crashing-thread backtrace. - **`po` for everything in LLDB** — it fails on Swift structs/enums/optionals. Default to `v`. - **Print-debug cycles** (edit → build → run → navigate, 3–5 min each) where a breakpoint would cost 30 seconds. - **Force-unwrapping `[weak self]`** (`self!`) — turns a leak into a crash without fixing either. The canonical pair is `[weak self]` + `guard let self else { return }`. - **Debugging a Release build** by default — variables are optimized out, code is reordered. Use Debug, or per-file `-Onone` (see the playbook's Release-crash section). - **Suppressing an exception** (force-continuing past it) instead of fixing the throw. - **Fixing the symptom, not the root cause** — rounding away a money float, catching-and-ignoring a decode error, adding `.id(UUID())` to "fix" a list reset (that *causes* resets — use a stable id). - **Marking the bug fixed without re-running the repro.** Bytes-on-disk is not a passing test. ## References (read the matching file before acting) - [references/lldb-playbook.md](references/lldb-playbook.md) — the full LLDB command reference: `v`/`p`/`po`/`expr` decision, breakpoints (conditional, symbolic, logpoints, watchpoints, ObjC-selector, by-address), thread/frame navigation, hang triage, async-concurrency debugging, **triggering/expiring a BGTask on demand** (`_simulate*` SPI), Release-only crash hunting + register reading, `.lldbinit` aliases, and the "LLDB is broken" fix table. - [references/crash-log-triage.md](references/crash-log-triage.md) — **getting the log off a device you can't attach to** (Console.app / Analytics Data / live fault predicate) + symbolication + the complete crash-code → fix-guidance table (every `pattern_tag`/Apple code), the classifier's quantified heuristics (guard-page, ≥100 `AG::Graph::update`, crashed-thread-only window), the **why-`do/catch`-didn't-catch** Swift/ObjC matrix, **PII scrub-before-sharing** key list, `xcsym` workflow (now incl. MetricKit JSON + `.xccrashpoint`) + exit codes, dSYM resolution, manual `.ips` parsing, and the `asc` TestFlight CLI. - [references/ios-bug-catalog.md](references/ios-bug-catalog.md) — the symptom→root-cause→fix→prevention catalog of the most common iOS runtime bugs (SwiftUI state ownership, navigation, sheets, `@MainActor`/`Sendable`, Core Data/SwiftData threading, retain cycles, URLSession, ATS, Codable, deep links, keyboard) plus **App Intents** errors (Siri/Shortcuts/Spotlight), the **fake-fetch/mock-data** detector, **sandbox inspection** (UserDefaults plist + SQLite WAL/SHM), and "database is locked". - [references/swiftui-crash-patterns.md](references/swiftui-crash-patterns.md) — the AsyncRenderer dynamic-color crash full investigation, the **empty-array-subscript-in-`body`-before-`onAppear`** push-render trap, the **compile-time "unable to type-check in reasonable time" TupleView** failure, and the other top SwiftUI runtime traps. - [references/objc-runtime-debugging.md](references/objc-runtime-debugging.md) — for any crash whose stack has `___forwarding___` / `unrecognized selector` you never wrote: the message-forwarding path (where to breakpoint), the swizzler-broke-lazy-accessors playbook, the three `___forwarding___` console signatures, reading args from registers (dual ARM64/x86-64 table + `po (char *)`), and source-less breakpoint forms. - [references/memory-and-leaks.md](references/memory-and-leaks.md) — retain-cycle hunting (Instruments, `deinit` logging, `[weak self]`/`[unowned self]`), the **SwiftUI-specific leak family** (`.refreshable`, searchable+refreshable, NavigationStack toggle, iOS 17.0–17.1 sheet regression, EnvironmentObject, UIHostingController), **framework cycle wrappers** (WKWebView `LeakAvoider`, CADisplayLink `WeakTarget`, AVPlayer observer), **automated runtime/CI leak detection** (`trackForMemoryLeaks`, zero-dep `LeakDetector`, LifetimeTracker), the **`.memgraph` CLI reference-tracing** 9-step procedure, and the jetsam-OOM follow-up.
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub