
Swift Diagnostics
- 150 installs
- 222 repo stars
- Updated January 18, 2026
- johnrogers/claude-swift-engineering
Interpret Swift compiler errors, runtime crashes, and Instruments output to fix build failures and stabilize iOS apps before release.
About
Equips Claude to debug Swift and iOS issues: decode compiler diagnostics, analyze crash reports, use Instruments for performance and memory, and propose minimal fixes that unblock mobile builds heading to TestFlight or App Store release.
- Compiler error interpretation
- Crash log and symbolication help
- Instruments profiling guidance
- Concurrency and memory diagnostics
- Regression reproduction steps
Swift Diagnostics by the numbers
- 150 all-time installs (skills.sh)
- +2 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #211 of 596 Debugging skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/johnrogers/claude-swift-engineering --skill swift-diagnosticsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 150 |
|---|---|
| repo stars | ★ 222 |
| Last updated | January 18, 2026 |
| Repository | johnrogers/claude-swift-engineering ↗ |
What it does
Interpret Swift compiler errors, runtime crashes, and Instruments output to fix build failures and stabilize iOS apps before release.
Files
Swift Diagnostics
Systematic debugging workflows for iOS/macOS development. These patterns help identify root causes in minutes rather than hours by following structured diagnostic approaches.
Reference Loading Guide
ALWAYS load reference files if there is even a small chance the content may be required. It's better to have the context than to miss a pattern or make a mistake.
| Reference | Load When |
|---|---|
| [Navigation](references/navigation.md) | NavigationStack not responding, unexpected pops, deep link failures |
| [Build Issues](references/build-issues.md) | SPM resolution, "No such module", dependency conflicts |
| [Memory](references/memory.md) | Retain cycles, memory growth, deinit not called |
| [Build Performance](references/build-performance.md) | Slow builds, Derived Data issues, Xcode hangs |
| [Xcode Debugging](references/xcode-debugging.md) | LLDB commands, breakpoints, view debugging |
Core Workflow
1. Identify symptom category - Navigation, build, memory, or performance 2. Load the relevant reference - Each has diagnostic decision trees 3. Run mandatory first checks - Before changing any code 4. Follow the decision tree - Reach diagnosis in 2-5 minutes 5. Apply fix and verify - One fix at a time, test each
Key Principle
80% of "mysterious" issues stem from predictable patterns:
- Navigation: Path state management or destination placement
- Build: Stale caches or dependency resolution
- Memory: Timer/observer leaks or closure captures
- Performance: Environment problems, not code bugs
Diagnose systematically. Never guess.
Common Mistakes
1. Skipping mandatory first checks — Jumping straight to code changes before running diagnostics (clean build, restart simulator, restart Xcode) means you'll chase ghosts. Always start with the mandatory checks.
2. Changing multiple things at once — "Let me delete DerivedData AND restart simulator AND kill Xcode" means you can't isolate which fix actually worked. Change one variable at a time.
3. Assuming you know the cause — "NavigationStack stopped working, must be my reducer" — actually it was stale DerivedData. Diagnostic trees prevent assumptions. Follow the tree, don't guess.
4. Missing memory basics — Calling deinit not being called is a retain cycle, but beginners often blame architecture. Use Instruments to verify leaks before refactoring. Data, not intuition.
5. Not isolating the problem — Testing with your whole app complicates diagnosis. Create a minimal reproducible example with just the problematic feature. Isolation reveals root causes.
Build Issues Diagnostics
Systematic debugging for SPM resolution, "No such module", and dependency conflicts. 80% of persistent build failures are dependency resolution issues, not code bugs.
Diagnostic Decision Table
| Error | Likely Cause | First Check |
|---|---|---|
| "No such module" after adding package | SPM cache stale | Clear package caches |
| "Multiple commands produce" | Duplicate file in targets | Check target membership |
| Build works locally, fails on CI | Environment/cache difference | Compare Podfile.lock |
| SPM resolution hangs | Package cache corruption | Delete .build and DerivedData |
| Framework version conflicts | Transitive dependency issue | Check Package.resolved |
Mandatory First Checks
# 1. Check Derived Data size (>10GB = stale)
du -sh ~/Library/Developer/Xcode/DerivedData
# 2. Check for zombie xcodebuild processes
ps aux | grep xcodebuild | grep -v grep
# 3. List available schemes
xcodebuild -listDecision Tree
Build failing?
|-- "No such module XYZ"?
| |-- After adding SPM package? -> Clean + reset package caches
| |-- After pod install? -> Check Podfile.lock conflicts
| |-- Framework not found? -> Check FRAMEWORK_SEARCH_PATHS
|
|-- "Multiple commands produce"?
| |-- Duplicate files in target membership -> Check File Inspector
|
|-- SPM resolution hangs?
| |-- Clear package caches + Derived Data
|
|-- Version conflicts?
|-- Use dependency resolution strategies belowQuick Fixes
SPM Package Not Found
# Nuclear clean
rm -rf ~/Library/Developer/Xcode/DerivedData
rm -rf ~/Library/Caches/org.swift.swiftpm
# Reset packages in project
xcodebuild -resolvePackageDependencies
# Clean build
xcodebuild clean build -scheme YourSchemeCocoaPods Conflicts
# Check what versions were installed
cat Podfile.lock | grep -A 2 "PODS:"
# Clean reinstall
rm -rf Pods/
rm Podfile.lock
pod install
# Always open workspace (not project)
open YourApp.xcworkspaceMultiple Commands Produce Error
1. Open Xcode 2. Select file in navigator 3. File Inspector > Target Membership 4. Uncheck duplicate targets 5. Or: Build Phases > Copy Bundle Resources > remove duplicates
Framework Search Paths
# Show all build settings
xcodebuild -showBuildSettings -scheme YourScheme | grep FRAMEWORK_SEARCH_PATHSFix in Xcode: 1. Target > Build Settings 2. Search "Framework Search Paths" 3. Add: $(PROJECT_DIR)/Frameworks (recursive)
Dependency Resolution Strategies
Strategy 1: Lock to Specific Versions
# Podfile - exact versions
pod 'Alamofire', '5.8.0'
pod 'SwiftyJSON', '~> 5.0.0' # Any 5.0.x// Package.swift - exact versions
.package(url: "...", exact: "1.2.3")Strategy 2: Use Version Ranges
// Package.swift
.package(url: "...", from: "1.2.0") // 1.2.0 and higher
.package(url: "...", .upToNextMajor(from: "1.0.0")) // 1.x.x but not 2.0Strategy 3: Reset SPM Resolution
# Clear package caches
rm -rf .build
rm Package.resolved
# Re-resolve
swift package resolveDebug vs Release Differences
# Compare configurations
xcodebuild -showBuildSettings -configuration Debug > debug.txt
xcodebuild -showBuildSettings -configuration Release > release.txt
diff debug.txt release.txtCommon culprits:
- SWIFT_OPTIMIZATION_LEVEL (-Onone vs -O)
- ENABLE_TESTABILITY (YES in Debug, NO in Release)
- DEBUG preprocessor flag
Command Reference
# CocoaPods
pod install # Install dependencies
pod update # Update to latest versions
pod outdated # Check for updates
pod deintegrate # Remove CocoaPods from project
# Swift Package Manager
swift package resolve # Resolve dependencies
swift package update # Update dependencies
swift package show-dependencies # Show dependency tree
xcodebuild -resolvePackageDependencies # Xcode's SPM resolve
# Xcode Build
xcodebuild clean # Clean build folder
xcodebuild -list # List schemes and targets
xcodebuild -showBuildSettings # Show all build settingsCommon Mistakes
Not Committing Lockfiles
# BAD: .gitignore includes lockfiles
Podfile.lock
Package.resolved
# These should be committed for reproducible buildsUsing "Latest" Version
# BAD: No version specified
pod 'Alamofire' # Breaking changes when updated
# GOOD: Explicit version
pod 'Alamofire', '~> 5.8'Opening Project Instead of Workspace
# BAD (with CocoaPods)
open YourApp.xcodeproj
# GOOD
open YourApp.xcworkspaceVerification Checklist
After applying fix:
- [ ] Build succeeds with clean Derived Data
- [ ] All dependencies resolve to expected versions
- [ ] Both Debug and Release configurations build
- [ ] CI builds match local builds
- [ ] Lockfiles committed to source control
Build Performance Diagnostics
Systematic debugging for slow builds, Derived Data issues, and Xcode hangs. 80% of "mysterious" build issues are environment problems, not code bugs.
Diagnostic Decision Table
| Symptom | Likely Cause | First Check |
|---|---|---|
| Build takes 10+ minutes | Stale Derived Data | Check DerivedData size |
| "Build succeeded" but old code runs | Cached build artifact | Delete Derived Data |
| Xcode beach balls during build | Zombie xcodebuild processes | Check process list |
| Simulator stuck at splash | Simulator in bad state | simctl list devices |
| Intermittent build failures | Environment corruption | Full clean rebuild |
Mandatory First Checks
# 1. Check for zombie processes
ps aux | grep -E "xcodebuild|Simulator" | grep -v grep
# 2. Check Derived Data size (>10GB = stale)
du -sh ~/Library/Developer/Xcode/DerivedData
# 3. Check simulator states
xcrun simctl list devices | grep -E "Booted|Booting|Shutting Down"What these tell you:
- 0 processes + small DerivedData + no stuck sims -> Environment clean
- 10+ processes OR >10GB DerivedData OR simulators stuck -> Clean first
Decision Tree
Build/performance problem?
|-- BUILD FAILED with no details?
| |-- Clean Derived Data -> rebuild
|
|-- Build succeeds but old code executes?
| |-- Delete Derived Data -> rebuild (2-5 min fix)
|
|-- Build intermittent (sometimes succeeds/fails)?
| |-- Clean Derived Data -> rebuild
|
|-- Xcode hangs during build?
| |-- Check for zombie xcodebuild processes
| |-- killall -9 xcodebuild
|
|-- "Unable to boot simulator"?
| |-- xcrun simctl shutdown all
| |-- xcrun simctl erase <device-uuid>
|
|-- Tests hang indefinitely?
|-- Check simctl list -> reboot simulatorQuick Fixes
Clean Derived Data
# Delete all Derived Data
rm -rf ~/Library/Developer/Xcode/DerivedData/*
# Also clean project-specific build folders
rm -rf .build/ build/
# Clean and rebuild
xcodebuild clean -scheme YourScheme
xcodebuild build -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'Kill Zombie Processes
# Kill all xcodebuild processes
killall -9 xcodebuild
# Verify they're gone
ps aux | grep xcodebuild | grep -v grep
# Kill Simulator if stuck
killall -9 SimulatorFix Simulator Issues
# Shutdown all simulators
xcrun simctl shutdown all
# If simctl fails, force quit first
killall -9 Simulator
xcrun simctl shutdown all
# List simulators to find problematic one
xcrun simctl list devices
# Erase specific simulator
xcrun simctl erase <device-uuid>
# Boot fresh simulator
xcrun simctl boot "iPhone 16 Pro"Reset SPM Caches
# Clear SPM caches
rm -rf ~/Library/Caches/org.swift.swiftpm
# Reset package dependencies
xcodebuild -resolvePackageDependencies
# Full clean rebuild
rm -rf ~/Library/Developer/Xcode/DerivedData/*
xcodebuild clean build -scheme YourSchemeIdentifying Build Time Hotspots
# Enable build timing
defaults write com.apple.dt.Xcode ShowBuildOperationDuration -bool YES
# Restart Xcode, build, check activity log
# Xcode > Report Navigator > Build logXcode Build Commands
# List available schemes
xcodebuild -list
# Show build settings
xcodebuild -showBuildSettings -scheme YourScheme
# Verbose build (more diagnostics)
xcodebuild -verbose build -scheme YourScheme
# Build for testing only (faster iteration)
xcodebuild build-for-testing -scheme YourScheme
# Run tests without rebuilding
xcodebuild test-without-building -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'
# Run specific test only
xcodebuild test -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:YourTests/SpecificTestClassCrash Log Analysis
# Find recent crashes
ls -lt ~/Library/Logs/DiagnosticReports/*.crash | head -5
# View crash log
cat ~/Library/Logs/DiagnosticReports/YourApp-*.crash | head -100
# Symbolicate address (if you have .dSYM)
atos -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp \
-arch arm64 0x<address>Environment Reset (Nuclear Option)
When nothing else works:
# 1. Quit Xcode
osascript -e 'quit app "Xcode"'
# 2. Kill all related processes
killall -9 xcodebuild Simulator
# 3. Clean all caches
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build/ build/
# 4. Reset simulators
xcrun simctl shutdown all
xcrun simctl erase all
# 5. Reopen Xcode
open -a Xcode YourProject.xcodeprojCommon Error Patterns
| Error | Fix |
|---|---|
| BUILD FAILED (no details) | Delete Derived Data |
| Unable to boot simulator | xcrun simctl erase <uuid> |
| No such module | Clean + delete Derived Data |
| Tests hang | Check simctl list, reboot simulator |
| Stale code executing | Delete Derived Data |
Common Mistakes
- Debugging code before checking environment - Always run mandatory steps first
- Ignoring simulator states - "Booting" can hang 10+ minutes
- Assuming git changes caused problem - Derived Data caches old builds
- Running full test suite when one test fails - Use
-only-testing
Verification Checklist
After applying fix:
- [ ] No zombie xcodebuild processes
- [ ] Derived Data under 5GB
- [ ] Simulators all in Shutdown state
- [ ] Clean build succeeds
- [ ] Correct code executes (not cached)
- [ ] Tests pass consistently
Memory Diagnostics
Systematic debugging for retain cycles, memory leaks, and deallocation issues. 90% of memory leaks follow 3 patterns: timer leaks, observer leaks, and closure captures.
Diagnostic Decision Table
| Symptom | Likely Cause | Diagnostic Tool |
|---|---|---|
| Memory grows 50MB -> 100MB -> 200MB | Retain cycle or timer leak | Memory Graph Debugger |
| App crashes after 10-15 minutes | Progressive memory leak | Instruments Allocations |
| deinit never called | Strong reference cycle | Memory Graph Debugger |
| Memory spike on specific action | Collection/closure leak | Allocations + filtering |
| Memory stays high after dismissing view | ViewController not deallocating | Add deinit logging |
Mandatory First Checks
// 1. Add deinit logging to suspected class
class PlayerViewModel: ObservableObject {
deinit {
print("PlayerViewModel deallocated")
}
}
// 2. Test deallocation
var vm: PlayerViewModel? = PlayerViewModel()
vm = nil // Should print "deallocated"# 3. Check device logs for memory warnings
# Connect device, open Xcode Console (Cmd+Shift+2)
# Look for: "Memory pressure critical", "Jetsam killed"
# 4. Check memory baseline
# Xcode > Product > Profile > Memory
# Perform action 5 times, check if memory keeps growingDecision Tree
Memory growing?
|-- Progressive growth every minute?
| |-- Timer or notification leak -> Check Pattern 1 & 2
|
|-- Spike when action performed?
| |-- Check if operation runs multiple times
| |-- Spike then flat? -> Probably normal caching
|
|-- deinit not called?
| |-- Use Memory Graph Debugger
| |-- Look for purple/red circles with warning badge
|
|-- Can't tell from inspection?
|-- Use Instruments > Allocations
|-- Track object counts over timeCommon Leak Patterns
Pattern 1: Timer Leaks (Most Common)
// WRONG - Timer never invalidated
class PlayerViewModel: ObservableObject {
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.update()
}
// Timer never stopped -> keeps firing forever
}
}
// CORRECT - Invalidate in deinit
class PlayerViewModel: ObservableObject {
private var timer: Timer?
func start() {
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.update()
}
}
func stop() {
timer?.invalidate()
timer = nil
}
deinit {
timer?.invalidate()
timer = nil
}
}Pattern 2: Observer Leaks
// WRONG - Observer never removed
class PlayerViewModel: ObservableObject {
init() {
NotificationCenter.default.addObserver(
self,
selector: #selector(handleChange),
name: .audioRouteChanged,
object: nil
)
}
}
// CORRECT - Use Combine (auto-cleanup)
class PlayerViewModel: ObservableObject {
private var cancellables = Set<AnyCancellable>()
init() {
NotificationCenter.default.publisher(for: .audioRouteChanged)
.sink { [weak self] _ in
self?.handleChange()
}
.store(in: &cancellables)
}
}Pattern 3: Closure Capture Leaks
// WRONG - Closure captures self strongly
class ViewController: UIViewController {
var callbacks: [() -> Void] = []
func addCallback() {
callbacks.append {
self.refresh() // Strong capture
}
}
}
// CORRECT - Use weak self
class ViewController: UIViewController {
var callbacks: [() -> Void] = []
func addCallback() {
callbacks.append { [weak self] in
self?.refresh()
}
}
deinit {
callbacks.removeAll()
}
}Pattern 4: Delegate Cycles
// WRONG - Strong delegate reference
class Player {
var delegate: PlayerDelegate? // Strong reference
}
class Controller: PlayerDelegate {
var player: Player?
init() {
player = Player()
player?.delegate = self // Creates cycle
}
}
// CORRECT - Weak delegate
class Player {
weak var delegate: PlayerDelegate?
}Using Memory Graph Debugger
1. Run app in Xcode simulator 2. Debug > Memory Graph Debugger (or toolbar icon) 3. Wait for graph to generate (5-10 seconds) 4. Look for purple/red circles with warning badge 5. Click to see retain cycle chain
Example output:
PlayerViewModel
^ strongRef from: progressTimer
^ strongRef from: TimerClosure
^ CYCLE DETECTEDUsing Instruments Allocations
1. Product > Profile (Cmd+I) 2. Select "Allocations" template 3. Perform action 5-10 times 4. Check: Does memory line keep going UP?
- YES -> Leak confirmed
- NO -> Probably not a leak
Time -->
Memory
| -------- <- Memory keeps growing (LEAK)
| /
| /
| /
+----------
vs normal:
| -------- <- Memory plateaus (OK)
| /
|/
+----------PhotoKit Request Leaks
// WRONG - Requests accumulate without cancellation
func loadImage(asset: PHAsset) {
imageManager.requestImage(for: asset, ...) { image, _ in
self.imageView.image = image
}
}
// CORRECT - Cancel in prepareForReuse
class PhotoCell: UICollectionViewCell {
private var requestID: PHImageRequestID = PHInvalidImageRequestID
func configure(asset: PHAsset) {
if requestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(requestID)
}
requestID = imageManager.requestImage(for: asset, ...) { [weak self] image, _ in
self?.imageView.image = image
}
}
override func prepareForReuse() {
super.prepareForReuse()
if requestID != PHInvalidImageRequestID {
PHImageManager.default().cancelImageRequest(requestID)
requestID = PHInvalidImageRequestID
}
}
}Quick Reference
| Leak Type | Detection | Fix |
|---|---|---|
| Timer | deinit not called | invalidate() in deinit |
| Observer | Memory grows steadily | Use Combine + cancellables |
| Closure | Memory Graph shows cycle | [weak self] capture |
| Delegate | Both objects stay alive | weak var delegate |
| Image requests | Memory spikes on scroll | Cancel in prepareForReuse |
Verification Checklist
After applying fix:
- [ ] deinit prints when expected
- [ ] Memory stays flat in Instruments
- [ ] No purple/red warnings in Memory Graph
- [ ] App doesn't crash after extended use
- [ ] Memory drops under simulated pressure (Xcode > Debug > Simulate Memory Warning)
Navigation Diagnostics
Systematic debugging for NavigationStack issues. 85% of navigation problems stem from path state management, view identity, or destination placement.
Diagnostic Decision Table
| Observation | Diagnosis | Next Step |
|---|---|---|
| onChange never fires on tap | NavigationLink not in NavigationStack | Check view hierarchy |
| onChange fires but view doesn't push | navigationDestination not found | Check destination placement |
| Pushes then immediately pops | View identity issue or path reset | Check @State location |
| Path changes unexpectedly | External code modifying path | Add logging to find source |
| Deep link doesn't navigate | Timing issue or wrong thread | Check MainActor isolation |
| State lost on tab switch | NavigationStack shared across tabs | Use separate stacks per tab |
Mandatory First Checks
Run these BEFORE changing code:
// 1. Add NavigationPath logging
NavigationStack(path: $path) {
RootView()
.onChange(of: path.count) { oldCount, newCount in
print("Path changed: \(oldCount) -> \(newCount)")
}
}
// 2. Verify navigationDestination is evaluated
.navigationDestination(for: Recipe.self) { recipe in
let _ = print("Destination for: \(recipe.name)")
RecipeDetail(recipe: recipe)
}
// 3. Test minimal case in isolation
NavigationStack {
NavigationLink("Test", value: "test")
.navigationDestination(for: String.self) { str in
Text("Pushed: \(str)")
}
}Decision Tree
Navigation problem?
|-- Link tap does nothing?
| |-- onChange fires? -> Check navigationDestination placement
| |-- onChange silent? -> Link outside NavigationStack
|
|-- Unexpected pop back?
| |-- Immediate after push? -> Path recreated (check @State location)
| |-- Random timing? -> External code modifying path
|
|-- Deep link fails?
| |-- URL received? -> Check MainActor for path.append
| |-- URL not received? -> Check URL scheme in Info.plist
|
|-- State lost on tab switch?
|-- Same path for all tabs? -> Each tab needs own NavigationStackCommon Patterns
Pattern 1: Link Outside NavigationStack
// WRONG - Link outside stack
VStack {
NavigationLink("Go", value: "test") // Won't work
NavigationStack {
Text("Root")
}
}
// CORRECT - Link inside stack
NavigationStack {
VStack {
NavigationLink("Go", value: "test")
Text("Root")
}
.navigationDestination(for: String.self) { Text($0) }
}Pattern 2: Destination in Lazy Container
// WRONG - Destination may not be loaded
LazyVStack {
ForEach(items) { item in
NavigationLink(item.name, value: item)
.navigationDestination(for: Item.self) { /* ... */ }
}
}
// CORRECT - Destination outside lazy container
LazyVStack {
ForEach(items) { item in
NavigationLink(item.name, value: item)
}
}
.navigationDestination(for: Item.self) { item in
ItemDetail(item: item)
}Pattern 3: Path Recreated Every Render
// WRONG - Path reset on every body evaluation
struct ContentView: View {
var body: some View {
let path = NavigationPath() // Recreated!
NavigationStack(path: .constant(path)) { /* ... */ }
}
}
// CORRECT - @State persists across renders
struct ContentView: View {
@State private var path = NavigationPath()
var body: some View {
NavigationStack(path: $path) { /* ... */ }
}
}Pattern 4: Path Modified Off MainActor
// WRONG - May fail silently
func loadAndNavigate() async {
let recipe = await fetchRecipe()
path.append(recipe) // Not on MainActor
}
// CORRECT - Explicit MainActor
@MainActor
func loadAndNavigate() async {
let recipe = await fetchRecipe()
path.append(recipe)
}Pattern 5: Deep Link Timing
// WRONG - NavigationStack may not exist yet
.onOpenURL { url in
handleDeepLink(url) // Too early on cold start
}
// CORRECT - Queue until ready
@State private var pendingDeepLink: URL?
@State private var isReady = false
var body: some View {
NavigationStack(path: $path) {
RootView()
.onAppear {
isReady = true
if let url = pendingDeepLink {
handleDeepLink(url)
pendingDeepLink = nil
}
}
}
.onOpenURL { url in
if isReady {
handleDeepLink(url)
} else {
pendingDeepLink = url
}
}
}Pattern 6: Shared NavigationStack Across Tabs
// WRONG - All tabs share navigation state
NavigationStack(path: $path) {
TabView {
Tab("Home") { HomeView() }
Tab("Settings") { SettingsView() }
}
}
// CORRECT - Each tab has own stack
TabView {
Tab("Home", systemImage: "house") {
NavigationStack {
HomeView()
}
}
Tab("Settings", systemImage: "gear") {
NavigationStack {
SettingsView()
}
}
}Type Mismatch Debugging
// Check: Value type must exactly match destination type
NavigationLink(recipe.name, value: recipe) // Recipe type
// This won't work if destination is for Recipe.ID
.navigationDestination(for: Recipe.ID.self) { id in // Wrong!
RecipeDetail(id: id)
}
// Types must match
.navigationDestination(for: Recipe.self) { recipe in // Correct
RecipeDetail(recipe: recipe)
}Verification Checklist
After applying fix:
- [ ] onChange(of: path) fires when expected
- [ ] Destination print statement executes
- [ ] Navigation persists (doesn't pop back)
- [ ] Works on cold start (deep links)
- [ ] State preserved across tab switches
Xcode Debugging Reference
LLDB commands, breakpoints, and view debugging techniques for iOS/macOS development.
LLDB Quick Reference
Basic Commands
# Print variable
po myVariable
p myVariable
# Print with format
p/x myInt # Hexadecimal
p/t myInt # Binary
p/d myInt # Decimal
# Print object description
po self
po myArray
# Expression evaluation
expr myVariable = 5
expr self.isLoading = trueInspecting Objects
# Print all properties
po self.debugDescription
# Print specific property
po self.viewModel.state
# Print array contents
po myArray.map { $0.name }
# Print dictionary
po myDict.keys
po myDict["key"]SwiftUI Debugging
# Print view hierarchy (from any breakpoint in a View)
po Self._printChanges()
# Check @State value
po _myStateVariable.wrappedValue
# Check @Binding
po _myBinding.wrappedValueMemory Inspection
# Check memory address
p unsafeBitCast(myObject, to: Int.self)
# Check reference count
po CFGetRetainCount(myObject as CFTypeRef)
# Print type
po type(of: myObject)Breakpoint Techniques
Conditional Breakpoints
1. Set breakpoint (click line number gutter) 2. Right-click breakpoint > Edit Breakpoint 3. Add condition:
myVariable == 5myArray.count > 10self.state == .loading
Action Breakpoints
Execute code without stopping:
1. Edit Breakpoint 2. Add Action > Debugger Command 3. Enter: po "Value is: \(myVariable)" 4. Check "Automatically continue"
Exception Breakpoints
Catch crashes before they happen:
1. Debug Navigator > + button 2. Add Exception Breakpoint 3. Choose: All Exceptions or Objective-C only
Symbolic Breakpoints
Break on any method call:
1. Debug Navigator > + button 2. Add Symbolic Breakpoint 3. Symbol: -[UIViewController viewDidLoad] 4. Or: UIApplication.shared
View Debugging
Debug View Hierarchy
1. Run app 2. Debug > View Debugging > Capture View Hierarchy 3. Or click cube icon in debug toolbar
What to Look For
- Overlapping views - Views stacked unexpectedly
- Constraint issues - Ambiguous layout warnings
- Hidden views - Views with alpha 0 or isHidden true
- Off-screen content - Views positioned outside bounds
Runtime Attribute Inspection
In the view debugger: 1. Select view in 3D hierarchy 2. Object Inspector shows all properties 3. Check frame, bounds, constraints
View Debugging Commands
# Print view hierarchy
po self.view.recursiveDescription()
# Print responder chain
po self.responderChain()
# Highlight view (in simulator)
expr self.view.layer.borderWidth = 2
expr self.view.layer.borderColor = UIColor.red.cgColorNetwork Debugging
Print Network Requests
// Add to URLSession configuration
#if DEBUG
URLSession.shared.configuration.protocolClasses?.insert(NetworkLogger.self, at: 0)
#endifLLDB Network Inspection
# Print URL request
po request.url
po request.httpMethod
po String(data: request.httpBody ?? Data(), encoding: .utf8)
# Print response
po response.statusCode
po String(data: responseData, encoding: .utf8)Thread Debugging
Check Current Thread
# Print current thread
thread info
# Print all threads
thread list
# Print backtrace
bt
bt allMain Thread Checker
Enable in scheme: 1. Edit Scheme > Run > Diagnostics 2. Check "Main Thread Checker"
Catches UI updates from background threads.
Performance Debugging
Time Profiler in LLDB
# Measure execution time
expr let start = CFAbsoluteTimeGetCurrent()
# ... execute code ...
expr print("Time: \(CFAbsoluteTimeGetCurrent() - start)")Memory Debugging
Enable in scheme: 1. Edit Scheme > Run > Diagnostics 2. Check "Malloc Stack Logging" 3. Check "Zombie Objects" (for EXC_BAD_ACCESS)
Common Debugging Scenarios
Scenario: View Not Appearing
# Check if view is in hierarchy
po self.view.superview
# Check frame
po self.view.frame
# Check if hidden
po self.view.isHidden
po self.view.alpha
# Check constraints
po self.view.constraintsScenario: Button Not Responding
# Check if user interaction enabled
po button.isUserInteractionEnabled
# Check if enabled
po button.isEnabled
# Check gesture recognizers
po button.gestureRecognizers
# Check if obscured
po self.view.hitTest(touchPoint, with: nil)Scenario: Data Not Loading
# Check network reachability
po NetworkMonitor.shared.isConnected
# Check API response
po lastResponse?.statusCode
po String(data: lastResponseData, encoding: .utf8)
# Check decoding
expr try JSONDecoder().decode(MyModel.self, from: data)Useful Xcode Shortcuts
| Action | Shortcut |
|---|---|
| Toggle breakpoint | Cmd + \ |
| Step over | F6 |
| Step into | F7 |
| Step out | F8 |
| Continue | Cmd + Ctrl + Y |
| View debugger | Cmd + Shift + D |
| Memory graph | Cmd + Shift + M |
| Debug navigator | Cmd + 7 |
Debug Console Tips
# Clear console
Cmd + K
# Search console output
Cmd + F
# Copy console selection
Cmd + C
# Increase console font
Cmd + + (plus)Verification Checklist
After debugging session:
- [ ] Remove debug print statements
- [ ] Disable unnecessary breakpoints
- [ ] Turn off diagnostic options in scheme (for production)
- [ ] Remove any
exprmodifications made during debugging