Debugging and Instruments Contents LLDB Debugging Memory Debugging Hang Diagnostics Build Failure Triage Instruments Overview Common Mistakes Review Checklist References LLDB Debugging Essential Commands (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. Breakpoint Management (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 Expression Evaluation (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. Watchpoints (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. Symbolic Breakpoints 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 . Memory Debugging Memory Graph Debugger Workflow Run the app in Debug configuration. Reproduce the suspected leak (navigate to a screen, then back). Tap the Memory Graph button in Xcode's debug bar. Look for purple warning icons — these indicate leaked objects. Select a leaked object to see its reference graph and backtrace. Enable Malloc Stack Logging (Scheme > Diagnostics) before running so the Memory Graph shows allocation backtraces. Common Retain Cycle Patterns 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
- (
- )
- }
- Instruments: Allocations and Leaks
- Allocations template
-
- Track memory growth over time. Use the
- "Mark Generation" feature to isolate allocations created between
- user actions (e.g., open/close a screen).
- Leaks template
- Automatically detects reference cycles at runtime. Run alongside Allocations for a complete picture. Filter by your app's module name to exclude system allocations. Malloc Stack Logging Enable in Scheme > Run > Diagnostics > Malloc Stack Logging (All Allocations). This records the call stack for every allocation, letting the Memory Graph Debugger and leaks CLI show where objects were created.
CLI leak detection
leaks --atExit -- ./MyApp.app/MyApp
Symbolicate with dSYMs for readable stacks
- Hang Diagnostics
- Identifying Main Thread Hangs
- A hang occurs when the main thread is blocked for > 250ms (noticeable) or
- 1s (severe). Common detection tools:
- Thread Checker
- (Xcode Diagnostics): warns about non-main-thread UI calls
- os_signpost
- and
- OSSignposter
- mark intervals for Instruments MetricKit hang diagnostics: production hang detection (see metrickit-diagnostics 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 ) } Using the Time Profiler Product > Profile (Cmd+I) to launch Instruments. Select the Time Profiler template. Record while reproducing the slow interaction. Focus on the main thread — sort by "Weight" to find hot paths. Check "Hide System Libraries" to see only your code. Double-click a heavy frame to jump to source. Common Hang Causes 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 Build Failure Triage Reading Compiler Diagnostics Start from the first error — subsequent errors are often cascading. Search for the error code (e.g., error: cannot convert ) in the build log. Use Report Navigator (Cmd+9) for the full build log with timestamps. SPM Dependency Resolution
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 Module Not Found / Linker Errors 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_PATHS OTHER_LDFLAGS SWIFT_INCLUDE_PATHS BUILD_LIBRARY_FOR_DISTRIBUTION (for XCFrameworks) Instruments Overview Template Selection Guide 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 Core Animation Frame drops, off-screen rendering, blending issues Energy Log Battery drain, background energy impact File Activity Excessive disk I/O, slow file operations System Trace Thread scheduling, syscalls, virtual memory faults xctrace CLI for CI Profiling
Record a trace from the command line
xcrun xctrace record --device "My iPhone" \ --template "Time Profiler" \ --output profile.trace \ --launch 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
xctrace
in CI pipelines to catch performance regressions
automatically. Compare exported metrics between builds.
Common Mistakes
DON'T: Use print() for debugging instead of os.Logger
print()
output is not filterable, has no log levels, and is not
automatically stripped from release builds. It pollutes the console and
makes it impossible to isolate relevant output.
// WRONG — unstructured, not filterable, stays in release builds
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 automatically excluded from release builds.
DON'T: Forget to enable Malloc Stack Logging before memory debugging
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
DON'T: Debug optimized code expecting full variable visibility
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 "