skills/debugging-instruments/SKILL.md
Debug iOS apps and profile performance using LLDB, Memory Graph Debugger, and Instruments. Use when diagnosing crashes, memory leaks, retain cycles, main thread hangs, slow rendering, build failures, or when profiling CPU, memory, energy, and network usage.
npx skillsauth add dpearson2699/swift-ios-skills debugging-instrumentsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Diagnose crashes, memory leaks, retain cycles, main thread hangs, and performance bottlenecks in iOS apps using LLDB, Memory Graph Debugger, and Instruments. Covers breakpoint workflows, memory graph analysis, hang detection, build failure triage, and Instruments profiling for CPU, memory, energy, and network.
(lldb) po myObject # Print object description (calls debugDescription)
(lldb) p myInt # Print with type info (uses LLDB formatter)
(lldb) v myLocal # Frame variable — fast, no code execution
(lldb) bt # Backtrace current thread
(lldb) bt all # Backtrace all threads
(lldb) frame select 3 # Jump to frame #3 in the backtrace
(lldb) thread list # List all threads and their states
(lldb) thread select 4 # Switch to thread #4
Use v over po when you only need a local variable value — it does not
execute code and cannot trigger side effects.
(lldb) br set -f ViewModel.swift -l 42 # Break at file:line
(lldb) br set -n viewDidLoad # Break on function name
(lldb) br set -S setValue:forKey: # Break on ObjC selector
(lldb) br modify 1 -c "count > 10" # Add condition to breakpoint 1
(lldb) br modify 1 --auto-continue true # Log and continue (logpoint)
(lldb) br command add 1 # Attach commands to breakpoint
> po self.title
> continue
> DONE
(lldb) br disable 1 # Disable without deleting
(lldb) br delete 1 # Remove breakpoint
(lldb) expr myArray.count # Evaluate Swift expression
(lldb) e -l swift -- import UIKit # Import framework in LLDB
(lldb) e -l swift -- self.view.backgroundColor = .red # Modify state at runtime
(lldb) e -l objc -- (void)[CATransaction flush] # Force UI update after changes
After modifying a view property in the debugger, call CATransaction.flush()
to see the change immediately without resuming execution.
(lldb) w set v self.score # Break when score changes
(lldb) w set v self.score -w read # Break when score is read
(lldb) w modify 1 -c "self.score > 100" # Conditional watchpoint
(lldb) w list # Show active watchpoints
(lldb) w delete 1 # Remove watchpoint
Watchpoints are hardware-backed (limited to ~4 on ARM). Use them to find unexpected mutations — the debugger stops at the exact line that changes the value.
Set breakpoints on methods without knowing the file. Useful for framework or system code:
(lldb) br set -n "UIViewController.viewDidLoad"
(lldb) br set -r ".*networkError.*" # Regex on symbol name
(lldb) br set -n malloc_error_break # Catch malloc corruption
(lldb) br set -n UIViewAlertForUnsatisfiableConstraints # Auto Layout issues
In Xcode, use the Breakpoint Navigator (+) to add symbolic breakpoints for
common diagnostics like -[UIApplication main] or swift_willThrow.
Enable Malloc Stack Logging (Scheme > Diagnostics) before running so the Memory Graph shows allocation backtraces.
Closure capturing self strongly:
// LEAK — closure holds strong reference to self
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // strong capture of self
}
}
}
// FIXED — use [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}
Strong delegate reference:
// LEAK — strong delegate creates a cycle
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // should be weak
}
// FIXED — weak delegate
class DataManager {
weak var delegate: DataDelegate?
}
Timer retaining target:
// LEAK — Timer.scheduledTimer retains its target
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// FIXED — use closure-based API with [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
For leak or memory-growth triage, pair the tools: use Allocations Mark Generation before and after the reproduction step to prove retained growth, then use Memory Graph Debugger to inspect object ownership and Malloc Stack Logging to recover allocation call stacks.
Enable in Scheme > Run > Diagnostics > Malloc Stack Logging. This records
allocation backtraces so the Memory Graph Debugger, Allocations instrument,
and exported .memgraph files can show where objects were created.
# Inspect an exported memory graph from Xcode or Instruments
leaks MyApp.memgraph
For discrete interactions, delays under 100 ms are rarely noticeable; a few hundred milliseconds can make an app feel unresponsive. Apple developer tools typically start reporting main-run-loop busy periods over 250 ms. Common detection tools:
OSSignposter: mark intervals for Instrumentsmetrickit skill for MXHangDiagnostic)import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "DataLoad")
func loadData() async {
let state = signposter.beginInterval("loadData")
let result = await fetchFromNetwork()
signposter.endInterval("loadData", state)
process(result)
}
| Cause | Symptom | Fix |
|-------|---------|-----|
| Synchronous I/O on main thread | Network/file reads block UI | Move to Task { } or background actor |
| Lock contention | Main thread waiting on a lock held by background work | Use actors or reduce lock scope |
| Layout thrashing | Repeated layoutSubviews calls | Batch layout changes, avoid forced layout |
| JSON parsing large payloads | UI freezes during data load | Parse on a background thread |
| Synchronous image decoding | Scroll jank on image-heavy lists | Use AsyncImage or decode off main thread |
error: cannot convert) in the build log.# Common: version conflict
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# Fix: check Package.resolved and update version ranges
# Reset package caches if needed:
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
swift package resolve
| Error | Check |
|-------|-------|
| No such module 'Foo' | Target membership, import paths, framework search paths |
| Undefined symbol | Linking phase missing framework, wrong architecture |
| duplicate symbol | Two targets define same symbol; check for ObjC naming collisions |
Build settings to inspect first:
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION (for XCFrameworks)| Template | Use When | |----------|----------| | Time Profiler | CPU is high, UI feels slow, need to find hot code paths | | Allocations | Memory grows over time, need to track object lifetimes | | Leaks | Suspect retain cycles or abandoned objects | | Network | Inspecting HTTP request/response timing and payloads | | SwiftUI | Profiling view body evaluations and update frequency | | Animation Hitches / Core Animation instruments | Frame drops, hitches, blending, and commit/render work | | Power Profiler | Battery drain, thermal pressure, background energy impact | | File Activity | Excessive disk I/O, slow file operations | | System Trace | Thread scheduling, syscalls, virtual memory faults |
# Record a trace from the command line
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--instrument "Allocations" \
--output profile.trace \
--launch -- /path/to/MyApp.app
# Export trace data as XML for automated analysis
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# List available templates
xcrun xctrace list templates
# List connected devices
xcrun xctrace list devices
Use one --template per recording; add extra instruments with
--instrument. Use xctrace in CI pipelines to catch performance regressions
automatically. Compare exported metrics between builds.
print() output is unstructured, has no subsystem/category or privacy
metadata, and is harder to filter than unified logging.
// WRONG — unstructured and not filterable by subsystem/category
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// CORRECT — structured logging with Logger
import os
let logger = Logger(subsystem: "com.example.app", category: "UI")
logger.debug("Button tapped, state: \(viewModel.state, privacy: .public)")
logger.info("Network response received, bytes: \(data.count)")
Logger messages appear in Console.app with filtering by subsystem and
category, and .debug messages are written to the in-memory log store only (not persisted to disk in release builds).
Without Malloc Stack Logging, the Memory Graph Debugger shows leaked objects but cannot display allocation backtraces, making it difficult to find the code that created them.
// WRONG — open Memory Graph without enabling Malloc Stack Logging
// Result: leaked objects visible but no allocation backtrace
// CORRECT — enable BEFORE running:
// Scheme > Run > Diagnostics > check "Malloc Stack Logging: All Allocations"
// Then run, reproduce the leak, and open Memory Graph
In Release (optimized) builds, the compiler may inline functions, eliminate variables, and reorder code. LLDB cannot display optimized-away values.
// WRONG — profiling with Debug build, debugging with Release build
// Debug builds: extra runtime checks distort perf measurements
// Release builds: variables show as "<optimized out>" in debugger
// CORRECT approach:
// Debugging: use Debug configuration (full symbols, no optimization)
// Profiling: use Release configuration (realistic performance)
Breaking on every iteration wastes time and makes it hard to find the specific case you care about.
// WRONG — breakpoint on line inside loop, stops 10,000 times
for item in items {
process(item) // breakpoint here stops on EVERY item
}
// CORRECT — use a conditional breakpoint:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// Or in Xcode: right-click breakpoint > Edit > add Condition
Thread Sanitizer (TSan) warnings indicate data races that may only crash intermittently. Treat them as real bugs unless you have isolated a tool issue.
// WRONG — ignoring TSan warning about concurrent access
var cache: [String: Data] = [:] // accessed from multiple threads
// CORRECT — protect shared mutable state
actor CacheActor {
var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { cache[key] }
func set(_ key: String, _ value: Data) { cache[key] = value }
}
Enable TSan: Scheme > Run > Diagnostics > Thread Sanitizer. For iOS, iPadOS, tvOS, visionOS, and watchOS apps, run TSan in Simulator; Apple documents device support only for 64-bit macOS apps.
When correcting another diagnostic plan, explicitly check these points:
os.Logger instead of print() for diagnostic outputweak var to prevent retain cycles[weak self] capture lists[weak self]OSSignposter used for custom performance intervalsdevelopment
Implement, review, or improve data visualizations using Swift Charts. Use when building bar, line, area, point, pie, donut, or iOS 26 3D charts; when adding chart selection, scrolling, annotations, axes, scales, legends, or foregroundStyle grouping; when plotting functions with BarPlot, LinePlot, AreaPlot, PointPlot, Chart3D, or SurfacePlot; or when creating heat maps, Gantt charts, grouped bars, sparklines, threshold lines, or spatial visualizations.
data-ai
Select, implement, or migrate between app architecture patterns for Apple platform apps. Use when choosing between MV (Model-View with @Observable), MVVM, MVI, TCA (The Composable Architecture), Clean Architecture, VIPER, or Coordinator patterns; when evaluating architecture fit for a feature's complexity; when migrating from one pattern to another; or when reviewing whether an app's current architecture is appropriate. Scoped to Apple-platform patterns using Swift 6.3, SwiftUI, and UIKit.
development
Apply Swift API Design Guidelines to name, label, and document Swift APIs. Covers argument label rules (prepositional phrase rule, grammatical phrase rule, first-label omission), mutating/nonmutating pair naming (-ed/-ing participle pattern, form- prefix, sort/sorted, formUnion/union), side-effect naming (noun for pure, verb for mutating), documentation comment structure (summary by declaration kind, O(1) complexity rule), clarity at call site, role-based naming, protocol naming (-able/-ible/-ing), default arguments over method families, casing conventions, and terminology. Use when designing new Swift APIs, reviewing naming and argument labels, writing documentation comments, or refactoring for call site clarity.
development
Implement, review, or improve in-app purchases and subscriptions using StoreKit 2. Use when building paywalls with SubscriptionStoreView or ProductView, processing transactions with Product and Transaction APIs, verifying entitlements, handling purchase flows (consumable, non-consumable, auto-renewable), implementing offer codes or promotional/win-back/introductory offers, managing subscription status and renewal state, setting up StoreKit testing with configuration files, or integrating Family Sharing, Ask to Buy, refund handling, and billing retry logic.