Loading...
Loading...
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 skill4agent add dpearson2699/swift-ios-skills debugging-instruments(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 #4vpo(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 changesCATransaction.flush()(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(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-[UIApplication main]swift_willThrow// 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()
}
}// 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?
}// 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()
}leaks# CLI leak detection
leaks --atExit -- ./MyApp.app/MyApp
# Symbolicate with dSYMs for readable stacks1s (severe). Common detection tools:
OSSignpostermetrickit-diagnosticsMXHangDiagnosticimport 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 |
| Lock contention | Main thread waiting on a lock held by background work | Use actors or reduce lock scope |
| Layout thrashing | Repeated | 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 |
error: cannot convert# 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 |
|---|---|
| Target membership, import paths, framework search paths |
| Linking phase missing framework, wrong architecture |
| Two targets define same symbol; check for ObjC naming collisions |
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION| 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 |
# 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 devicesxctraceprint()// 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.debug// 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// 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)// 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// 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 }
}os.Loggerprint()weak var[weak self][weak self]OSSignposterreferences/lldb-patterns.mdreferences/instruments-guide.md