
Axiom Build
- 700 installs
- 1.1k repo stars
- Updated August 3, 2026
- charleswiltgen/axiom
axiom-build is a Claude Code skill that compiles, scheme-selects, and archives Xcode projects via xcodebuild or Xcode for developers who need reliable iOS and macOS builds with signing and Swift package resolution.
About
axiom-build is a Claude Code skill for compiling, scheme-selecting, and archiving Xcode projects on iOS and macOS. It drives xcodebuild or Xcode directly, resolving code signing, target selection, Swift Package Manager dependencies, and multi-platform build settings that often block local builds. Developers reach for axiom-build when an agent must produce a signed archive, switch schemes, or unblock CI-style compilation without manually debugging Xcode configuration. The skill focuses on build automation artifacts rather than app store submission or UI design.
- xcodebuild CLI workflows
- Scheme and configuration selection
- Code signing and provisioning
- Swift Package Manager resolution
- Simulator vs device destinations
Axiom Build by the numbers
- 700 all-time installs (skills.sh)
- Ranked #261 of 1,039 Mobile Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-buildAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 700 |
|---|---|
| repo stars | ★ 1.1k |
| Last updated | August 3, 2026 |
| Repository | charleswiltgen/axiom ↗ |
How do you archive an Xcode project with signing?
Compile, scheme-select, and archive Xcode projects via xcodebuild or Xcode, resolving signing, targets, Swift packages, and multi-platform build settings for iOS and macOS.
Who is it for?
Developers maintaining native iOS or macOS apps who need agent-driven xcodebuild compilation with signing and multi-target settings handled systematically.
Skip if: Developers building web-only, Android-only, or React Native projects without an Xcode workspace should skip axiom-build.
When should I use this skill?
The user mentions xcodebuild, Xcode archive, iOS or macOS signing errors, scheme selection, or Swift package build failures.
What you get
Compiled Xcode build outputs, archived app bundles, resolved scheme and target configuration, and validated Swift package dependencies.
- Compiled build artifacts
- Archived app bundle
- Resolved build configuration
Files
Build & Environment
You MUST use this skill for ANY build, environment, or Xcode-related issue before debugging application code.
When to Use
Use this router when you encounter:
- Build failures (
BUILD FAILED, compilation errors, linker errors) - Test crashes or hangs
- Simulator issues (won't boot, device errors)
- Xcode misbehavior (stale builds, zombie processes)
- Dependency conflicts (CocoaPods, SPM)
- Build performance issues (slow compilation)
- Environment issues before debugging code
Routing Logic
This router invokes specialized skills based on the specific issue:
1. Environment-First Issues → xcode-debugging
Triggers:
BUILD FAILEDwithout obvious code cause- Tests crash in clean project
- Simulator hangs or won't boot
- "No such module" after SPM changes
- Zombie
xcodebuildprocesses - Stale builds (old code still running)
- Clean build differs from incremental build
- Device Hub / predicted-vs-built issues in Xcode 27 (
OS27) - Reproducing a device-only bug on a simulator (Device Hub) (
OS27)
Why xcode-debugging first: 90% of mysterious issues are environment, not code. Check this BEFORE debugging code.
Invoke: skills/xcode-debugging.md
---
2. Slow Builds → build-performance
Triggers:
- Compilation takes too long
- Type checking bottlenecks
- Want to optimize build time
- Build Timeline shows slow phases
Invoke: skills/build-performance.md
---
3. SPM Dependency Conflicts → spm-conflict-resolver (Agent)
Triggers:
- SPM resolution failures
- "No such module" after adding package
- Duplicate symbol linker errors
- Version conflicts between packages
- Swift 6 package compatibility issues
- Package.swift / Package.resolved conflicts
Why spm-conflict-resolver: Specialized agent that analyzes Package.swift and Package.resolved to diagnose and resolve Swift Package Manager conflicts.
Invoke: Launch spm-conflict-resolver agent
---
4. Security & Privacy Audit → security-privacy-scanner (Agent)
Triggers:
- App Store submission prep
- Privacy Manifest requirements (iOS 17+)
- Hardcoded credentials in code
- Sensitive data storage concerns
- ATS violations
- Required Reason API declarations
Why security-privacy-scanner: Specialized agent that scans for security vulnerabilities and privacy compliance issues.
Invoke: Launch security-privacy-scanner agent or /axiom:audit security
---
5. iOS 17→18 Modernization → modernization-helper (Agent)
Triggers:
- Migrate ObservableObject to @Observable
- Update @StateObject to @State
- Adopt modern SwiftUI patterns
- Deprecated API cleanup
- iOS 17+ migration
Why modernization-helper: Specialized agent that scans for legacy patterns and provides migration paths with code examples.
Invoke: Launch modernization-helper agent or /axiom:audit modernization
---
6. Build Failure Auto-Fix → build-fixer (Agent)
Triggers:
- BUILD FAILED with no clear error details
- Build sometimes succeeds, sometimes fails
- App builds but runs old code
- "Unable to boot simulator" error
- Want automated environment-first diagnostics
Why build-fixer: Autonomous agent that checks zombie processes, Derived Data, SPM cache, and simulator state before investigating code. Saves 30+ minutes on environment issues.
Invoke: Launch build-fixer agent or /axiom:fix-build
---
7. Slow Build Optimization → build-optimizer (Agent)
Triggers:
- Builds take too long
- Want to identify slow type checking
- Expensive build phase scripts
- Suboptimal build settings
- Want parallelization opportunities
Why build-optimizer: Scans Xcode projects for build performance optimizations — slow type checking, expensive scripts, suboptimal settings — to reduce build times by 30-50%.
Invoke: Launch build-optimizer agent or /axiom:optimize-build
---
8. General Dependency Issues → build-debugging
Triggers:
- CocoaPods resolution failures
- "Multiple commands produce" errors
- Framework version mismatches
- Non-SPM dependency graph conflicts
Invoke: skills/build-debugging.md
---
9. TestFlight Crash Triage → testflight-triage
Triggers:
- Beta tester reported a crash
- Crash reports in Xcode Organizer
- Crash logs aren't symbolicated
- TestFlight feedback with screenshots
- App was killed but no crash report
Why testflight-triage: Systematic workflow for investigating TestFlight crashes and reviewing beta feedback. Covers symbolication, crash interpretation, common patterns, and Claude-assisted analysis.
Invoke: See axiom-shipping (skills/testflight-triage.md)
---
10. App Store Connect Navigation → app-store-connect-ref
Triggers:
- How to find crashes in App Store Connect
- ASC metrics dashboard navigation
- Understanding crash-free users percentage
- Comparing crash rates between versions
- Exporting crash data from ASC
- App Store Connect API for crash data
Why app-store-connect-ref: Reference for navigating ASC crash analysis, metrics dashboards, and data export workflows.
Invoke: See axiom-shipping (skills/app-store-connect-ref.md)
---
11. Crash Log Analysis → crash-analyzer (Agent)
Triggers:
- User has .ips or .crash file to analyze
- User pasted crash report text
- Need to parse crash log programmatically
- Identify crash pattern from exception type
- Check symbolication status
Why crash-analyzer: Autonomous agent that parses crash reports, identifies patterns (null pointer, Swift runtime, watchdog, jetsam), and generates actionable analysis.
Invoke: Launch crash-analyzer agent or /axiom:analyze-crash
---
12. MetricKit API Reference → metrickit-ref
Triggers:
- MetricKit setup and subscription
- MXMetricPayload parsing (CPU, memory, launches, hitches)
- MXDiagnosticPayload parsing (crashes, hangs, disk writes)
- MXCallStackTree decoding and symbolication
- Field crash/hang collection
- Background exit metrics
Why metrickit-ref: Complete MetricKit API reference with setup patterns, payload parsing, and integration with crash reporting systems.
Invoke: See axiom-performance (skills/metrickit-ref.md)
---
13. Hang Diagnostics → hang-diagnostics
Triggers:
- App hangs or freezes
- Main thread blocked for >1 second
- UI unresponsive to touches
- Xcode Organizer shows hang diagnostics
- MXHangDiagnostic from MetricKit
- Watchdog terminations (app killed during launch/background transition)
Why hang-diagnostics: Systematic diagnosis of hangs with decision tree for busy vs blocked main thread, tool selection (Time Profiler, System Trace), and 8 common hang patterns with fixes.
Invoke: See axiom-performance (skills/hang-diagnostics.md)
---
14. Live Debugging → lldb
Triggers:
- Need to reproduce a crash interactively
- Want to set breakpoints and inspect state
- Crash report analyzed, now need live investigation
- Need to attach debugger to running app
Why lldb: Crash reports tell you WHAT crashed. LLDB tells you WHY.
Invoke: skills/lldb.md
---
16. Runtime Console Capture → xclog-ref
Triggers:
- Need to see what the app is logging at runtime
- App crashes but no crash report (need console output)
- Silent failures (network, data, auth) with no UI feedback
- Want to capture print()/os_log() output from simulator
- Need structured log output for analysis
- "What is the app printing?"
Why xclog-ref: Xcode's debug console isn't accessible externally. xclog combines simctl stdout/stderr with log stream JSON to capture everything print(), NSLog(), os_log(), and Logger emit — with structured fields (level, subsystem, category) for automated analysis.
Invoke: /axiom:console
---
15. Code Signing Issues → code-signing
Triggers:
- "No signing certificate found"
- "Provisioning profile doesn't include signing certificate"
- errSecInternalComponent in CI
- ITMS-90035 Invalid Signature on upload
- Ambiguous identity / multiple certificates
- Entitlement mismatch or missing capability
- Setting up CI/CD code signing (GitHub Actions, fastlane match)
- Certificate expired or revoked
Why code-signing: Code signing errors are NEVER code bugs — they are 100% configuration (certificates, profiles, entitlements, keychains). Diagnosing with CLI tools takes 5 minutes vs hours of guessing.
Invoke: See axiom-security (skills/code-signing.md) (workflows) or See axiom-security (skills/code-signing-diag.md) (troubleshooting)
---
Decision Tree
1. Mysterious/intermittent/clean build fails? → xcode-debugging (environment-first) 2. SPM dependency conflict? → spm-conflict-resolver (Agent) 3. CocoaPods/other dependency conflict? → build-debugging 4. Slow build time? → build-performance 5. Security/privacy/App Store prep? → security-privacy-scanner (Agent) 6. Want automated build fix (environment-first diagnostics)? → build-fixer (Agent) 7. Want build time optimization scan? → build-optimizer (Agent) 8. Modernization/deprecated APIs? → modernization-helper (Agent) 9. TestFlight crash/feedback? → testflight-triage 10. Navigating App Store Connect? → app-store-connect-ref 11. Have a crash log (.ips/.crash)? → crash-analyzer (Agent) 12. MetricKit setup/parsing? → metrickit-ref 13. App hang/freeze/watchdog? → hang-diagnostics 14. Need to reproduce crash interactively / inspect runtime state? → lldb 15. Code signing error (certificate, profile, entitlement, Keychain)? → code-signing / code-signing-diag 16. Need to see runtime console output (print/os_log)? → xclog-ref or /axiom:console
Anti-Rationalization
| Thought | Reality |
|---|---|
| "I know how to fix this linker error" | Linker errors have 4+ root causes. xcode-debugging diagnoses all in 2 min. |
| "Let me just clean the build folder" | Clean builds mask the real issue. xcode-debugging finds the root cause. |
| "It's just an SPM issue, I'll fix Package.swift" | SPM conflicts cascade. spm-conflict-resolver analyzes the full dependency graph. |
| "The simulator is just slow today" | Simulator issues indicate environment corruption. xcode-debugging checks systematically. |
| "I'll skip environment checks, it compiles locally" | Environment-first saves 30+ min. Every time. |
| "I'll read the crash report more carefully instead of reproducing" | Crash reports show WHAT crashed, not WHY. Reproducing in LLDB with breakpoints reveals the actual state. skills/lldb.md has the workflow. |
| "I know my certificate is fine, let me check the code" | Code signing errors are NEVER code bugs. 100% configuration. code-signing diagnoses with CLI in 5 min. |
| "I can't see what the app is logging without Xcode" | xclog captures print() + os_log from the simulator. Structured JSON output with level, subsystem, category. /axiom:console. |
When NOT to Use (Conflict Resolution)
Do NOT use axiom-build for these — use the correct router instead:
| Error Type | Correct Router | Why NOT axiom-build |
|---|---|---|
| Swift 6 concurrency errors | /skill axiom-concurrency | Code error, not environment |
| SwiftData migration errors | /skill axiom-data | Schema issue, not build environment |
| "Sending 'self' risks data race" | /skill axiom-concurrency | Language error, not Xcode issue |
| Type mismatch / compilation errors | Fix the code | These are code bugs |
axiom-build is for environment mysteries, not code errors:
- ✅ "No such module" when code is correct
- ✅ Simulator won't boot
- ✅ Clean build fails, incremental works
- ✅ Zombie xcodebuild processes
- ❌ Swift concurrency warnings/errors
- ❌ Database migration failures
- ❌ Type checking errors in valid code
Example Invocations
User: "My build failed with a linker error" → Invoke: skills/xcode-debugging.md (environment-first diagnostic)
User: "Builds are taking 10 minutes" → Invoke: skills/build-performance.md
User: "SPM won't resolve dependencies" → Invoke: spm-conflict-resolver agent
User: "Two packages require different versions of the same dependency" → Invoke: spm-conflict-resolver agent
User: "Duplicate symbol linker error" → Invoke: spm-conflict-resolver agent
User: "I need to prepare for App Store security review" → Invoke: security-privacy-scanner agent
User: "Do I need a Privacy Manifest?" → Invoke: security-privacy-scanner agent
User: "Are there hardcoded credentials in my code?" → Invoke: security-privacy-scanner agent
User: "How do I migrate from ObservableObject to @Observable?" → Invoke: modernization-helper agent
User: "Update my code to use modern SwiftUI patterns" → Invoke: modernization-helper agent
User: "Should I still use @StateObject?" → Invoke: modernization-helper agent
User: "A beta tester said my app crashed" → Invoke: See axiom-shipping (skills/testflight-triage.md)
User: "I see crashes in App Store Connect but don't know how to investigate" → Invoke: See axiom-shipping (skills/testflight-triage.md)
User: "My crash logs aren't symbolicated" → Invoke: See axiom-shipping (skills/testflight-triage.md)
User: "I need to review TestFlight feedback" → Invoke: See axiom-shipping (skills/testflight-triage.md)
User: "How do I find crashes in App Store Connect?" → Invoke: See axiom-shipping (skills/app-store-connect-ref.md)
User: "Where's the crash-free users metric in ASC?" → Invoke: See axiom-shipping (skills/app-store-connect-ref.md)
User: "How do I export crash data from App Store Connect?" → Invoke: See axiom-shipping (skills/app-store-connect-ref.md)
User: "Analyze this crash log" [pastes .ips content] → Invoke: crash-analyzer agent or /axiom:analyze-crash
User: "Parse this .ips file: ~/Library/Logs/DiagnosticReports/MyApp.ips" → Invoke: crash-analyzer agent or /axiom:analyze-crash
User: "Why did my app crash? Here's the report..." → Invoke: crash-analyzer agent or /axiom:analyze-crash
User: "How do I set up MetricKit to collect crash data?" → Invoke: See axiom-performance (skills/metrickit-ref.md)
User: "How do I parse MXDiagnosticPayload?" → Invoke: See axiom-performance (skills/metrickit-ref.md)
User: "What's in MXCallStackTree and how do I decode it?" → Invoke: See axiom-performance (skills/metrickit-ref.md)
User: "My app hangs sometimes" → Invoke: See axiom-performance (skills/hang-diagnostics.md)
User: "The main thread is blocked and UI is unresponsive" → Invoke: See axiom-performance (skills/hang-diagnostics.md)
User: "Xcode Organizer shows hang diagnostics for my app" → Invoke: See axiom-performance (skills/hang-diagnostics.md)
User: "My app was killed by watchdog during launch" → Invoke: See axiom-performance (skills/hang-diagnostics.md)
User: "I have a crash report and need to reproduce it in the debugger" → Invoke: skills/lldb.md
User: "How do I set breakpoints to catch this crash?" → Invoke: skills/lldb.md
User: "My build is failing with BUILD FAILED but no error details" → Invoke: build-fixer agent or /axiom:fix-build
User: "Build sometimes succeeds, sometimes fails" → Invoke: build-fixer agent or /axiom:fix-build
User: "How can I speed up my Xcode build times?" → Invoke: build-optimizer agent or /axiom:optimize-build
User: "No signing certificate found when I try to build" → Invoke: See axiom-security (skills/code-signing-diag.md)
User: "errSecInternalComponent in my GitHub Actions CI" → Invoke: See axiom-security (skills/code-signing-diag.md)
User: "How do I set up code signing for GitHub Actions?" → Invoke: See axiom-security (skills/code-signing.md)
User: "What is my app printing to the console?" → Invoke: /axiom:console
User: "I need to see the simulator console output" → Invoke: /axiom:console
User: "The app fails silently, no error in the UI" → Invoke: /axiom:console
Build Debugging
Overview
Check dependencies BEFORE blaming code. Core principle 80% of persistent build failures are dependency resolution issues (CocoaPods, SPM, framework conflicts), not code bugs.
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "I added a Swift Package but I'm getting 'No such module' errors. The package is in my Xcode project but won't compile."
→ The skill covers SPM resolution workflows, package cache clearing, and framework search path diagnostics
2. "The build is failing with 'Multiple commands produce' the same output file. How do I figure out which files are duplicated?"
→ The skill shows how to identify duplicate target membership and resolve file conflicts in build settings
3. "CocoaPods installed dependencies successfully but the build still fails. How do I debug CocoaPods issues?"
→ The skill covers Podfile.lock conflict resolution, linking errors, and version constraint debugging
4. "My build works on my Mac but fails on the CI server. Both machines have the latest Xcode. What's different?"
→ The skill explains dependency caching differences, environment-specific paths, and reproducible build strategies
5. "I'm getting framework version conflicts and I don't know which dependency is causing it. How do I resolve this?"
→ The skill demonstrates dependency graph analysis and version constraint resolution strategies for complex dependency trees
---
Red Flags — Dependency/Build Issues
If you see ANY of these, suspect dependency problem:
- "No such module" after adding package
- "Multiple commands produce" same output file
- Build succeeds on one machine, fails on another
- CocoaPods install succeeds but build fails
- SPM resolution takes forever or times out
- Framework version conflicts in error logs
Quick Decision Tree
Build failing?
├─ "No such module XYZ"?
│ ├─ After adding SPM package?
│ │ └─ Clean build folder + 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
├─ SPM resolution hangs?
│ └─ Clear package caches + derived data
└─ Version conflicts?
└─ Use dependency resolution strategies belowCommon Build Issues
Issue 1: SPM Package Not Found
Symptom: "No such module PackageName" after adding Swift Package
❌ WRONG:
# Rebuilding without cleaning
xcodebuild build✅ CORRECT:
# Reset package caches first
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 YourSchemeIssue 2: CocoaPods Conflicts
Symptom: Pod install succeeds but build fails with framework errors
Check Podfile.lock:
# See what versions were actually installed
cat Podfile.lock | grep -A 2 "PODS:"
# Compare with Podfile requirements
cat Podfile | grep "pod "Fix version conflicts:
# Podfile - be explicit about versions
pod 'Alamofire', '~> 5.8.0' # Not just 'Alamofire'
pod 'SwiftyJSON', '5.0.1' # Exact version if neededClean reinstall:
# Remove all pods
rm -rf Pods/
rm Podfile.lock
# Reinstall
pod install
# Open workspace (not project!)
open YourApp.xcworkspaceIssue 3: Multiple Commands Produce Error
Symptom: "Multiple commands produce '/path/to/file'"
Cause: Same file added to multiple targets or build phases
Fix: 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
Issue 4: Framework Search Paths
Symptom: "Framework not found" or "Linker command failed"
Check build settings:
# 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 path: $(PROJECT_DIR)/Frameworks (recursive) 4. Or: $(inherited) to inherit from project
Issue 5: SPM Version Conflicts
Symptom: Package resolution fails with version conflicts
See dependency graph:
# In project directory
swift package show-dependencies
# Or see resolved versions
cat Package.resolvedFix conflicts:
// Package.swift - be explicit
.package(url: "https://github.com/owner/repo", exact: "1.2.3") // Exact version
.package(url: "https://github.com/owner/repo", from: "1.2.0") // Minimum version
.package(url: "https://github.com/owner/repo", .upToNextMajor(from: "1.0.0")) // SemVerReset resolution:
# Clear package caches
rm -rf .build
rm Package.resolved
# Re-resolve
swift package resolveDependency Resolution Strategies
Strategy 1: Lock to Specific Versions
When stability matters more than latest features:
CocoaPods:
pod 'Alamofire', '5.8.0' # Exact version
pod 'SwiftyJSON', '~> 5.0.0' # Any 5.0.xSPM:
.package(url: "...", exact: "1.2.3")Strategy 2: Use Version Ranges
When you want bug fixes but not breaking changes:
CocoaPods:
pod 'Alamofire', '~> 5.8' # 5.8.x but not 5.9
pod 'SwiftyJSON', '>= 5.0', '< 6.0' # RangeSPM:
.package(url: "...", from: "1.2.0") // 1.2.0 and higher
.package(url: "...", .upToNextMajor(from: "1.0.0")) // 1.x.x but not 2.0.0Strategy 3: Fork and Pin
When you need custom modifications:
# Fork repo on GitHub
# Clone your fork
git clone https://github.com/yourname/package.git
# In Package.swift, use your fork
.package(url: "https://github.com/yourname/package", branch: "custom-fixes")Strategy 4: Exclude Transitive Dependencies
When a dependency's dependency conflicts:
SPM (not directly supported, use workarounds):
// Instead of this:
.package(url: "https://github.com/problematic/package")
// Fork it and remove the conflicting dependency from its Package.swiftCocoaPods:
# Exclude specific subspecs
pod 'Firebase/Core' # Not all of Firebase
pod 'Firebase/Analytics'Build Configuration Issues
Debug vs Release Differences
Symptom: Builds in Debug, fails in Release (or vice versa)
Check optimization settings:
# Compare Debug and Release settings
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
- Code signing settings
Workspace vs Project
Always open workspace with CocoaPods:
# ❌ WRONG
open YourApp.xcodeproj
# ✅ CORRECT
open YourApp.xcworkspaceCheck which you're building:
# For workspace
xcodebuild -workspace YourApp.xcworkspace -scheme YourScheme build
# For project only (no CocoaPods)
xcodebuild -project YourApp.xcodeproj -scheme YourScheme buildPressure Scenarios: When to Resist "Quick Fix" Advice
The Problem
Under deadline pressure, senior engineers and teammates provide "quick fixes" based on pattern-matching:
- "Just regenerate the lock file"
- "Increment the build number"
- "Delete DerivedData and rebuild"
These feel safe because they come from experience. But if the diagnosis is wrong, the fix wastes time you don't have.
Critical insight Time pressure makes authority bias STRONGER. You're more likely to trust advice when stressed.
Red Flags — STOP Before Acting
If you hear ANY of these, pause 5 minutes before executing:
- ❌ "This smells like..." (pattern-matching, not diagnosis)
- ❌ "Just..." (underestimating complexity)
- ❌ "This usually fixes it" (worked once ≠ works always)
- ❌ "You have plenty of time" (overconfidence about 24-hour turnaround)
- ❌ "This is safe" (regenerating lock files CAN break things)
Your brain under pressure Trusts these phrases because they sound confident. Doesn't ask "but do they have evidence THIS is the root cause?"
Mandatory Diagnosis Before "Quick Fix"
When someone senior suggests a fix under time pressure:
Step 1: Ask (Don't argue)
"I understand the pressure. Before we regenerate lock files,
can we spend 5 minutes comparing the broken build to our
working build? I want to know what we're fixing."Step 2: Demand Evidence
- "What makes you think it's a lock file issue?"
- "What changed between our last successful build and this failure?"
- "Can we see the actual error from App Store build vs our build?"
Step 3: Document the Gamble
If we try "pod install":
- Time to execute: 10 minutes
- Time to learn it failed: 24 hours (next submission cycle)
- Remaining time if it fails: 6 days
- Alternative: Spend 1-2 hours diagnosing first
Cost of being wrong with quick fix: High
Cost of spending 1 hour on diagnosis: LowStep 4: Push Back Professionally
"I want to move fast too. A 1-hour diagnosis now means we
won't waste another 24-hour cycle. Let's document what we're
testing before we submit."Why this works
- You're not questioning their expertise
- You're asking for evidence (legitimate request)
- You're showing you understand the pressure
- You're making the time math visible
Real-World Example: App Store Review Blocker
Scenario App rejected in App Store build, passes locally.
Senior says "Regenerate lock file and resubmit (7 days buffer)"
What you do
1. ❌ WRONG: Execute immediately, fail after 24 hours, now 6 days left 2. ✅ RIGHT: Spend 1 hour comparing builds first
Comparison checklist
Local build that works:
- Pod versions in Podfile.lock: [list them]
- Xcode version: [version]
- Derived Data: [timestamp]
- CocoaPods version: [version]
App Store build that fails:
- Pod versions used: [from error message]
- Build system: [App Store's environment]
- Differences: [explicitly document]After comparison
- If versions match: Lock file isn't the issue. Skip the quick fix.
- If versions differ: Now you understand what to fix.
Time saved 24 hours of wasted iteration.
When to Trust Quick Fixes (Rare)
Quick fixes are safe ONLY when:
- [ ] You've seen this EXACT error before (not "similar")
- [ ] You know the root cause (not "this usually works")
- [ ] You can reproduce it locally (so you know if fix worked)
- [ ] You have >48 hours buffer (so failure costs less)
- [ ] You documented the fix in case you need to explain it later
In production crises, NONE of these are usually true.
---
Testing Checklist
When Adding Dependencies
- [ ] Specify exact versions or ranges (not just latest)
- [ ] Check for known conflicts with existing deps
- [ ] Test clean build after adding
- [ ] Commit lockfile (Podfile.lock or Package.resolved)
When Builds Fail
- [ ] Run mandatory environment checks (xcode-debugging skill)
- [ ] Check dependency lockfiles for changes
- [ ] Verify using correct workspace/project file
- [ ] Compare working vs broken build settings
Before Shipping
- [ ] Test both Debug and Release builds
- [ ] Verify all dependencies have compatible licenses
- [ ] Check binary size impact of dependencies
- [ ] Test on clean machine or CI
Common Mistakes
❌ Not Committing Lockfiles
# ❌ BAD: .gitignore includes lockfiles
Podfile.lock
Package.resolvedWhy: Team members get different versions, builds differ
❌ Using "Latest" Version
# ❌ BAD: No version specified
pod 'Alamofire'Why: Breaking changes when dependency updates
❌ Mixing Package Managers
Project uses both:
- CocoaPods (Podfile)
- Carthage (Cartfile)
- SPM (Package.swift)Why: Conflicts are inevitable, pick one primary manager
❌ Not Cleaning After Dependency Changes
# ❌ BAD: Just rebuild
xcodebuild build
# ✅ GOOD: Clean first
xcodebuild clean build❌ Opening Project Instead of Workspace
When using CocoaPods, always open .xcworkspace not .xcodeproj
Command Reference
# CocoaPods
pod install # Install dependencies
pod update # Update to latest versions
pod update PodName # Update specific pod
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
swift package reset # Reset package cache
xcodebuild -resolvePackageDependencies # Xcode's SPM resolve
# Carthage
carthage update # Update dependencies
carthage bootstrap # Download pre-built frameworks
carthage build --platform iOS # Build for specific platform
# Xcode Build
xcodebuild clean # Clean build folder
xcodebuild -list # List schemes and targets
xcodebuild -showBuildSettings # Show all build settingsReal-World Impact
Before (trial-and-error with dependencies):
- Dependency issue: 2-4 hours debugging
- Clean builds not run consistently
- Version conflicts surprise team
- CI failures from dependency mismatches
After (systematic dependency management):
- Dependency issue: 15-30 minutes (check lockfile → resolve)
- Clean builds mandatory after dep changes
- Explicit version constraints prevent surprises
- CI matches local builds (committed lockfiles)
Key insight Lock down dependency versions early. Flexibility causes more problems than it solves.
Resources
Docs: swift.org/package-manager, /xcode/build-system
GitHub: Carthage/Carthage
Skills: axiom-build (skills/xcode-debugging.md)
---
History: See git log for changes
Build Performance Optimization
Overview
Systematic Xcode build performance analysis and optimization. Core principle: Measure before optimizing, then optimize the critical path first.
When to Use This Skill
- Build times have increased significantly
- Incremental builds taking too long
- Want to analyze Build Timeline
- Need to identify slow-compiling Swift code
- Optimizing CI/CD build times
- Build performance regression investigation
- Enabling Xcode 26 compilation caching
- Reducing module variants in explicitly built modules
- Understanding the three-phase build process (scan → modules → compile)
Quick Win: Run the Agent First
For automated scanning and quick wins:
/axiom:optimize-buildThe build-optimizer agent scans for common issues and provides immediate fixes. Use this skill for deep analysis.
The Build Performance Workflow
Step 1: Measure Baseline (Required)
Why: You can't improve what you don't measure. Baseline prevents placebo optimizations.
# Clean build (eliminates all caching)
xcodebuild clean build -scheme YourScheme
# Measure time
time xcodebuild build -scheme YourScheme
# Or use Xcode UI
Product → Perform Action → Build with Timing SummaryRecord:
- Total build time
- Incremental build time (change one file, rebuild)
- Which phase takes longest (compilation vs linking vs scripts)
Example baseline:
Clean build: 247 seconds
Incremental (1 file change): 12 seconds
Longest phase: Compile Swift sources (189s)Step 2: Analyze Build Timeline (Xcode 14+)
Access: 1. Build your project (Cmd+B) 2. Open Report Navigator (Cmd+9) 3. Select latest build 4. Show Assistant Editor (Cmd+Option+Return) 5. Build Timeline appears alongside build log
What to look for:
Critical Path (The Build's Speed Limit)
The critical path is the shortest possible build time with unlimited CPU cores. It's defined by the longest chain of dependent tasks.
┌─────────────────────────────────────────┐
│ Critical Path: A → B → C → D (120s) │
│ │
│ Task A: 30s ─────────┐ │
│ Task B: 40s ├─→ D: 20s │
│ Task C: 30s ─────────┘ │
│ │
│ Even with 100 CPUs, build takes 120s │
└─────────────────────────────────────────┘Goal: Shorten the critical path by breaking dependencies.
Timeline Red Flags
Empty vertical space: Tasks waiting for inputs
Timeline:
████████░░░░░░░░████████ ← Bad: idle cores waiting
████████████████████████ ← Good: continuous workLong horizontal bars: Slow individual tasks
Task A: ████████████████████ (45 seconds) ← Investigate
Task B: ███ (3 seconds) ← FineSerial target builds: Targets waiting unnecessarily
Framework: ████████░░░░░░░░░░ ← Waiting
App: ░░░░░░░░░░████████ ← Delayed
Better (parallel):
Framework: ████████
App: ░░░░████████████Step 3: Identify Bottlenecks (Decision Tree)
Is compilation the slowest phase? ├─ YES → Check type checking performance (Step 4) └─ NO → Is linking slow? ├─ YES → Check link dependencies (Step 5) └─ NO → Are scripts slow? ├─ YES → Optimize build phase scripts (Step 6) └─ NO → Check parallelization (Step 7)
Optimization Patterns
Pattern 1: Type Checking Performance (MEDIUM-HIGH IMPACT)
Symptom: "Compile Swift sources" takes >50% of build time.
Diagnosis:
Enable compiler warnings to find slow functions:
// Add to Debug build settings → Other Swift Flags
-warn-long-function-bodies 100
-warn-long-expression-type-checking 100Build → Xcode shows warnings:
MyView.swift:42: Function body took 247ms to type-check (limit: 100ms)
LoginViewModel.swift:18: Expression took 156ms to type-check (limit: 100ms)Fix slow type checking:
// ❌ SLOW - Complex type inference (247ms)
func calculateTotal(items: [Item]) -> Double {
return items
.filter { $0.isActive }
.map { $0.price * $0.quantity }
.reduce(0, +)
}
// ✅ FAST - Explicit types (12ms)
func calculateTotal(items: [Item]) -> Double {
let activeItems: [Item] = items.filter { $0.isActive }
let prices: [Double] = activeItems.map { $0.price * $0.quantity }
let total: Double = prices.reduce(0, +)
return total
}Common slow patterns:
- Complex chained operations without intermediate types
- Deeply nested closures
- Large literals (dictionaries, arrays)
- Operator overloading in complex expressions
Expected impact: 10-30% faster compilation for affected files.
---
Pattern 2: Build Phase Script Optimization (HIGH IMPACT)
Symptom: Build Timeline shows long script phases in Debug builds.
Common culprits:
- dSYM/Crashlytics uploads running in Debug
- Asset processing on every build
- Code generation scripts without caching
Fix: Make scripts conditional
# ❌ BAD - Runs in ALL configurations (adds 6+ seconds to debug builds)
#!/bin/bash
firebase crashlytics upload-symbols
# ✅ GOOD - Skip in Debug
#!/bin/bash
if [ "${CONFIGURATION}" = "Release" ]; then
firebase crashlytics upload-symbols
fi
# Example savings: 6.3 seconds per incremental debug buildScript Phase Sandboxing (Xcode 14+)
Enable to prevent data races and improve parallelization:
Build Settings → User Script Sandboxing → YESWhy: Forces you to declare inputs/outputs explicitly, enabling parallel execution.
# Script phase with proper inputs/outputs
Input Files:
$(SRCROOT)/input.txt
$(DERIVED_FILE_DIR)/checksum.txt
Output Files:
$(DERIVED_FILE_DIR)/output.html
# Now Xcode knows dependencies and can parallelize safelyParallel Script Execution:
Build Settings → FUSE_BUILD_SCRIPT_PHASES → YES⚠️ WARNING: Only enable if ALL scripts have correct inputs/outputs declared. Otherwise you'll get data races.
Expected impact: 5-10 seconds saved per incremental debug build.
---
Pattern 3: Compilation Mode Settings (CRITICAL)
Symptom: Incremental builds recompile entire modules.
Check current settings:
# In project.pbxproj
grep "SWIFT_COMPILATION_MODE" project.pbxprojOptimal configuration:
| Configuration | Setting | Why |
|---|---|---|
| Debug | singlefile (Incremental) | Only recompiles changed files |
| Release | wholemodule | Maximum optimization |
// ❌ BAD - Whole module in Debug
SWIFT_COMPILATION_MODE = wholemodule; // ALL configs
// ✅ GOOD - Incremental for Debug
Debug: SWIFT_COMPILATION_MODE = singlefile;
Release: SWIFT_COMPILATION_MODE = wholemodule;How to fix: 1. Project → Build Settings 2. Filter: "Compilation Mode" 3. Set Debug to "Incremental" 4. Set Release to "Whole Module"
Expected impact: 40-60% faster incremental debug builds.
---
Pattern 4: Build Active Architecture Only (HIGH IMPACT)
Symptom: Debug builds compile for multiple architectures (x86_64 + arm64).
Check:
grep "ONLY_ACTIVE_ARCH" project.pbxprojFix:
| Configuration | Setting | Why |
|---|---|---|
| Debug | YES | Only build for current device (arm64 OR x86_64) |
| Release | NO | Build universal binary |
How to fix: 1. Build Settings → "Build Active Architecture Only" 2. Set Debug to YES 3. Keep Release as NO
Expected impact: 40-50% faster debug builds (half the architectures).
---
Pattern 5: Debug Information Format (MEDIUM IMPACT)
Symptom: Debug builds generating dSYMs unnecessarily.
Optimal configuration:
| Configuration | Setting | Why |
|---|---|---|
| Debug | dwarf | Embedded debug info, faster |
| Release | dwarf-with-dsym | Separate dSYM for crash reporting |
# Check current
grep "DEBUG_INFORMATION_FORMAT" project.pbxprojHow to fix: 1. Build Settings → "Debug Information Format" 2. Set Debug to "DWARF" 3. Set Release to "DWARF with dSYM File"
Expected impact: 3-5 seconds saved per debug build.
---
Pattern 6: Target Parallelization (WWDC 2018-408)
Symptom: Build Timeline shows targets building sequentially when they could be parallel.
Check scheme configuration: 1. Product → Scheme → Edit Scheme 2. Build tab 3. Check "Parallelize Build" checkbox 4. Verify target order allows parallelization
Dependency graph example:
App ──┬──→ Framework A
└──→ Framework B
Framework A ──→ Utilities
Framework B ──→ UtilitiesTimeline (bad - serial):
Utilities: ████████░░░░░░░░░░░░░░
Framework A: ░░░░░░░░████████░░░░░░
Framework B: ░░░░░░░░░░░░░░░░████████
App: ░░░░░░░░░░░░░░░░░░░░░░████Timeline (good - parallel):
Utilities: ████████
Framework A: ░░░░░░░░████████
Framework B: ░░░░░░░░████████
App: ░░░░░░░░░░░░░░░░████Expected impact: Proportional to number of independent targets (e.g., 2 parallel targets = ~2x faster).
---
Pattern 7: Emit Module Optimization (Xcode 14+, Swift 5.7+)
What it is: Swift modules are produced separately from compilation, unblocking downstream targets faster.
Before (Xcode 13):
Framework: Compile ████████████ → Emit Module █
App: ░░░░░░░░░░░░░░░░░░░░░░░░░█████████
↑
Waiting for Framework compilation to finishAfter (Xcode 14+):
Framework: Compile ████████████
Emit Module ███
App: ░░░░░░███████████
↑
Starts as soon as module emittedAutomatic: No configuration needed, works in Xcode 14+ with Swift 5.7+.
Expected impact: Reduces idle time in multi-target builds by 20-40%.
---
Pattern 8: Eager Linking (Xcode 14+)
What it is: Linking can start before all compilation finishes if the module is ready.
Impact: Further reduces critical path in dependency chains.
Automatic: Works in Xcode 14+ automatically.
---
Pattern 9: Compilation Caching (Xcode 26+, CRITICAL)
What it is: Xcode 26 introduces compilation caching that reuses previously compiled artifacts across clean builds.
Build Settings:
Build Settings → COMPILATION_CACHE_ENABLE_CACHING → YESHow it works:
- Caches compilation results based on input file content and compiler flags
- Works across clean builds — even after
xcodebuild clean, cached artifacts can be reused - Significantly reduces CI/CD build times where clean builds are common
When to enable:
- CI/CD pipelines with frequent clean builds
- Teams sharing build artifacts
- Projects with stable dependencies
Verification:
# Build with caching enabled
xcodebuild build -scheme YourScheme \
COMPILATION_CACHE_ENABLE_CACHING=YES
# Check build log for cache informationCurrent limitations (Xcode 26):
- Swift Package Manager dependencies not yet cacheable
- CompileStoryboard, CompileXIB, DataModelCompile, Ld tasks not cacheable
- Cache requires time to populate on first run
Expected impact: 20-40% faster clean builds after initial cache population (up to 70%+ for favorable projects).
---
Pattern 10: Explicitly Built Modules (Xcode 16+, HIGH IMPACT)
What it is: Xcode splits module compilation into explicit build tasks instead of implicit on-demand compilation. Enabled by default for Swift in Xcode 26.
The Problem with Implicit Modules (Pre-Xcode 16):
When a compiler encounters an import, it builds the module on-demand:
Compile A.swift ─── needs UIKit ───→ (builds UIKit.pcm) ───→ continues
Compile B.swift ─── needs UIKit ───→ (waits for A to finish) ───→ uses cached
Compile C.swift ─── needs UIKit ───→ (waits) ───→ uses cachedProblems:
- One task blocks others waiting for the same module
- Non-deterministic: whoever gets there first builds it
- Build failures hard to reproduce (depends on task order)
Explicitly Built Modules Solution:
Xcode now separates compilation into three phases:
Phase 1: SCAN Phase 2: BUILD MODULES Phase 3: COMPILE
┌──────────────────┐ ┌──────────────────────┐ ┌──────────────────┐
│ Scan A.swift │ │ Build UIKit.pcm │ │ Compile A.swift │
│ Scan B.swift │ → │ Build Foundation.pcm │ → │ Compile B.swift │
│ Scan C.swift │ │ Build SwiftUI.pcm │ │ Compile C.swift │
└──────────────────┘ └──────────────────────┘ └──────────────────┘
(fast) (parallel) (parallel)Benefits:
- More reliable builds: Precise dependencies, deterministic build graphs
- More efficient scheduling: Build system knows exactly what's needed
- Better debugging: Debugger reuses built modules (no separate rebuild)
- Visible module tasks: See "Compile Clang Module" and "Compile Swift Module" in build log
Enable/Disable (if needed):
Build Settings → Explicitly Built Modules → YES (default in Xcode 26 for Swift)Module Variants (WWDC 2024-10171)
The same module may be built multiple times with different settings:
Build Log:
Compile Clang module 'UIKit' (hash: abc123) ← Variant 1
Compile Clang module 'UIKit' (hash: def456) ← Variant 2
Compile Swift module 'UIKit' (hash: ghi789) ← Variant 3Common causes of variants:
- Different preprocessor macros between targets
- Mixed C and Objective-C language modes
- Different C language versions (C11 vs C17)
- Disabling ARC on some targets
Diagnose variants: 1. Build with Timing Summary: Product → Perform Action → Build with Timing Summary 2. Filter build log: Type "modules report" in filter box 3. View Clang and Swift module reports showing variant counts
Reduce variants (unify settings at project/workspace level):
# Check for macro differences
grep "GCC_PREPROCESSOR_DEFINITIONS" project.pbxproj
# Move target-specific macros to project level where possible
Project → Build Settings → Preprocessor Macros → [unify here]Example (from WWDC 2024-10171):
Before: 4 UIKit variants (2 Swift × 2 Clang)
After: 2 UIKit variants (unified settings)
Impact: Fewer module builds = faster incremental buildsExpected impact: 10-30% faster builds by reducing duplicate module compilation.
Note: Swift Build (Xcode 26+): Xcode now uses Swift Build, Apple's open-source build engine. This provides more predictable builds, better SPM integration, and cross-platform support (Linux, Windows, Android). No configuration needed.
---
Measurement & Verification
Before and After Comparison
Required steps:
1. Baseline (before changes):
xcodebuild clean build -scheme YourScheme 2>&1 | tee baseline.log2. Apply ONE optimization at a time
3. Measure improvement:
xcodebuild clean build -scheme YourScheme 2>&1 | tee optimized.log4. Compare:
# Extract build time from logs
grep "Build succeeded" baseline.log
grep "Build succeeded" optimized.logExample:
Baseline: Build succeeded (247.3 seconds)
Optimized: Build succeeded (156.8 seconds)
Improvement: 90.5 seconds (36.6% faster)Build Timeline Visual Verification
Before optimization:
- Look for empty vertical space (idle cores)
- Long horizontal bars (slow tasks)
- Serial target builds
After optimization:
- Timeline should be more "filled"
- Shorter horizontal bars
- Parallel target builds
Critical path: Should be visibly shorter.
---
Real-World Optimization Examples
Example 1: Large iOS App (50+ source files)
Baseline:
- Clean build: 247 seconds
- Incremental (1 file): 12 seconds
Optimizations applied: 1. Debug compilation mode: singlefile (saved 89s) 2. Build Active Architecture: YES (saved 45s) 3. Conditional dSYM upload script (saved 6.3s per incremental)
Result:
- Clean build: 156 seconds (36% faster)
- Incremental: 5.7 seconds (52% faster)
---
Example 2: Multi-Framework Project
Baseline:
- 5 frameworks built serially
- Total: 189 seconds
Optimizations applied: 1. Enabled parallel builds in scheme 2. Fixed unnecessary dependencies 3. Emit module optimization (automatic in Xcode 14)
Result:
- Total: 94 seconds (50% faster)
- Critical path reduced from 189s to 94s
---
Common Pitfalls
Pitfall 1: Optimizing Without Measuring
Mistake: "I think this will help" → make change → no measurement.
Why bad: Placebo improvements, wasted time, actual regressions unnoticed.
Fix: Always measure before → change one thing → measure after.
---
Pitfall 2: Optimizing Release Builds for Speed
Mistake: Set Release to incremental compilation for "faster builds".
Why bad: Release builds should optimize for runtime performance, not build speed. You ship Release builds to users.
Fix: Only optimize Debug builds for speed. Keep Release optimized for runtime.
---
Pitfall 3: Breaking Dependencies for Parallelization
Mistake: Remove legitimate dependencies to "make builds parallel".
Why bad: Build errors, undefined behavior, race conditions.
Fix: Only parallelize truly independent targets. Use Build Timeline to identify safe opportunities.
---
Pitfall 4: Enabling FUSE_BUILD_SCRIPT_PHASES Without Sandboxing
Mistake: Enable parallel scripts but don't declare inputs/outputs.
Why bad: Data races, non-deterministic build failures, incorrect builds.
Fix: First enable ENABLE_USER_SCRIPT_SANDBOXING = YES, fix all errors, THEN enable FUSE_BUILD_SCRIPT_PHASES.
---
Troubleshooting
Problem: Builds Still Slow After Optimizations
Check: 1. Did you clean before measuring? (xcodebuild clean) 2. Are you measuring the right build? (Debug vs Release) 3. Is your machine thermal throttling? (Activity Monitor → CPU tab) 4. Are other apps using CPU? (Quit Xcode, Docker, VMs during measurement)
---
Problem: Build Timeline Shows No Parallelization
Check: 1. Scheme → Parallelize Build checked? 2. Are targets actually independent? (Check dependency graph) 3. Do targets have unnecessary explicit dependencies?
---
Problem: Type Checking Warnings Don't Appear
Check: 1. Added flags to correct configuration? (Debug, not Release) 2. Syntax correct? -warn-long-function-bodies 100 (with hyphen) 3. Building the right scheme? 4. Clean build to force recompilation
---
Advanced: Analyzing Build Logs
Extract Compilation Times
# Find slowest files to compile
xcodebuild -workspace YourApp.xcworkspace \
-scheme YourScheme \
clean build \
OTHER_SWIFT_FLAGS="-Xfrontend -debug-time-function-bodies" 2>&1 | \
grep ".[0-9]ms" | \
sort -nr | \
head -20Output:
247.3ms MyViewModel.swift:42:1 func calculateTotal
156.8ms LoginView.swift:18:3 var body
89.2ms NetworkManager.swift:67:1 func handleResponse
...Action: Add explicit types to slowest functions.
---
Extract Build Phase Times
# From build log
Build target 'MyApp' (project 'MyApp')
Compile Swift source files (128.4 seconds)
Link MyApp (12.3 seconds)
Run custom shell script (6.7 seconds)Action: Optimize the longest phase first.
---
Checklist: Build Performance Audit
Before considering your build optimized:
Measurement
- [ ] Measured baseline (clean + incremental)
- [ ] Verified improvement in Build Timeline
- [ ] Documented baseline → optimized comparison
Compilation Settings
- [ ] Debug uses incremental compilation
- [ ] Build Active Architecture = YES (Debug only)
- [ ] Debug uses DWARF (not dSYM)
- [ ] Type checking warnings enabled
- [ ] Fixed slow type-checking functions (>100ms)
Parallelization
- [ ] Parallelize Build enabled in scheme
- [ ] No unnecessary target dependencies
- [ ] Build phase scripts are conditional (skip in Debug when possible)
- [ ] Enabled script sandboxing if using parallel scripts
Xcode 26+ (if applicable)
- [ ] Compilation caching enabled for CI/CD (
COMPILATION_CACHE_ENABLE_CACHING) - [ ] Checked module variants (Modules Report in build log, see Pattern 10)
- [ ] Unified build settings at project level to reduce module variants
- [ ] Explicitly Built Modules enabled (default for Swift in Xcode 26)
---
Resources
WWDC: 2018-408, 2022-110364, 2024-10171, 2025-247
Docs: /xcode/improving-the-speed-of-incremental-builds, /xcode/building-your-project-with-explicit-module-dependencies
Tools: Xcode Build Timeline (Xcode 14+), Build with Timing Summary (Product → Perform Action), Modules Report (Xcode 16+), Instruments Time Profiler
---
Remember: Build performance optimization is about systematic measurement and targeted improvements. Optimize the critical path first, measure everything, and verify improvements in the Build Timeline.
LLDB Command Reference
Complete command reference for LLDB in Xcode. Organized by task so you can find the exact command you need.
For debugging workflows and decision trees, see See axiom-build (skills/lldb.md).
---
Part 1: Variable Inspection
v / frame variable
Reads memory directly. No compilation. Most reliable for Swift values.
(lldb) v # All variables in current frame
(lldb) v self # Self in current context
(lldb) v self.propertyName # Specific property
(lldb) v localVariable # Local variable
(lldb) v self.array[0] # Collection element
(lldb) v self._showDetails # SwiftUI @State backing store (underscore prefix)Flags:
| Flag | Effect |
|---|---|
-d run | Run dynamic type resolution (slower but more accurate) |
-T | Show types |
-R | Show raw (unformatted) output |
-D N | Limit depth of nested types to N levels |
-P N | Limit pointer depth to N levels |
-F | Flat output (no hierarchy) |
Limitations: Cannot evaluate expressions, computed properties, or function calls. Use p for those.
p / expression (with format)
Compiles and executes an expression. Shows formatted result.
(lldb) p self.computedProperty
(lldb) p items.count
(lldb) p someFunction()
(lldb) p String(describing: someValue)
(lldb) p (1...10).map { $0 * 2 }Result stored in numbered variables:
(lldb) p someValue
$R0 = 42
(lldb) p $R0 + 10
$R1 = 52po / expression --object-description
Calls debugDescription (or description) on the result.
(lldb) po myObject
(lldb) po error
(lldb) po notification.userInfo
(lldb) po NSHomeDirectory()When `po` adds value: Classes with CustomDebugStringConvertible, NSError, NSNotification, collections of objects.
When `po` fails: Swift structs without CustomDebugStringConvertible, protocol-typed values (use v instead — it performs iterative dynamic type resolution that po doesn't).
expression (full form)
Full expression evaluation with all options.
(lldb) expression self.view.backgroundColor = UIColor.red
(lldb) expression self.debugFlag = true
(lldb) expression myArray.append("test")
(lldb) expression CATransaction.flush() # Force UI update
(lldb) expression Self._printChanges() # SwiftUI debugFlags:
| Flag | Effect |
|---|---|
-l objc | Evaluate as Objective-C |
-l swift | Evaluate as Swift (default) |
-O | Object description (same as po) |
-i false | Stop on breakpoints hit during evaluation (default: ignore) |
-- | Separator between flags and expression |
ObjC expressions for Swift debugging:
(lldb) expr -l objc -- (void)[[[[[UIApplication sharedApplication] connectedScenes] anyObject] keyWindow] recursiveDescription]
(lldb) expr -l objc -- (void)[CATransaction flush]
(lldb) expr -l objc -- (int)[[UIApplication sharedApplication] _isForeground]-[UIApplication keyWindow] is API_DEPRECATED(ios(2.0,13.0)); use the scene-aware -[UIWindowScene keyWindow] reached via connectedScenes.
register read
Low-level register inspection:
(lldb) register read
(lldb) register read x0 x1 # Specific registers (ARM64)
(lldb) register read --all # All register sets---
Part 2: Breakpoints
Setting Breakpoints
(lldb) breakpoint set -f File.swift -l 42 # File + line
(lldb) b File.swift:42 # Short form
(lldb) breakpoint set -n methodName # By function name
(lldb) breakpoint set -n "MyClass.myMethod" # Qualified name
(lldb) breakpoint set -S layoutSubviews # ObjC selector
(lldb) breakpoint set -r "viewDid.*" # Regex on name
(lldb) breakpoint set -a 0x100abc123 # Memory addressConditional Breakpoints
(lldb) breakpoint set -f File.swift -l 42 -c "value == nil"
(lldb) breakpoint set -f File.swift -l 42 -c "index > 100"
(lldb) breakpoint set -f File.swift -l 42 -c 'name == "test"'Ignore Count
(lldb) breakpoint set -f File.swift -l 42 -i 50 # Skip first 50 hitsOne-Shot Breakpoints
(lldb) breakpoint set -f File.swift -l 42 -o # Delete after first hitBreakpoint Commands (Logpoints)
Add commands that execute when breakpoint hits:
(lldb) breakpoint command add 1
> v self.state
> p self.items.count
> continue
> DONEOr in one line:
(lldb) breakpoint command add 1 -o "v self.state"Exception Breakpoints
(lldb) breakpoint set -E swift # All Swift errors
(lldb) breakpoint set -E objc # All ObjC exceptions
# Filtering by exception name requires Xcode's GUI (Edit Breakpoint → Exception field)Symbolic Breakpoints
(lldb) breakpoint set -n UIViewAlertForUnsatisfiableConstraints # Auto Layout
(lldb) breakpoint set -n "-[UIApplication _run]" # App launch
(lldb) breakpoint set -n swift_willThrow # Swift throwManaging Breakpoints
(lldb) breakpoint list # List all
(lldb) breakpoint list -b # Brief format
(lldb) breakpoint enable 3 # Enable breakpoint 3
(lldb) breakpoint disable 3 # Disable breakpoint 3
(lldb) breakpoint delete 3 # Delete breakpoint 3
(lldb) breakpoint delete # Delete ALL (asks confirmation)
(lldb) breakpoint modify 3 -c "x > 10" # Add condition to existingWatchpoints
Break when a variable's memory changes:
(lldb) watchpoint set variable self.count # Watch for write
(lldb) watchpoint set variable -w read_write myGlobal # Watch for read or write
(lldb) watchpoint set expression -- &myVariable # Watch memory address
(lldb) watchpoint list # List all
(lldb) watchpoint delete 1 # Delete watchpoint 1
(lldb) watchpoint modify 1 -c "self.count > 10" # Add conditionNote: Hardware watchpoints are limited (~4 per process). Use sparingly.
---
Part 3: Thread & Backtrace
Backtraces
(lldb) bt # Current thread backtrace
(lldb) bt 10 # Limit to 10 frames
(lldb) bt all # All threads
(lldb) thread backtrace all # Same as bt allThread Navigation
(lldb) thread list # List all threads with state
(lldb) thread info # Current thread details + stop reason
(lldb) thread select 3 # Switch to thread 3Frame Navigation
(lldb) frame info # Current frame details
(lldb) frame select 5 # Jump to frame 5
(lldb) up # Go up one frame (toward caller)
(lldb) down # Shortcut: go down one frameThread Return (Skip Code)
Force an early return from the current function:
(lldb) thread return # Return void
(lldb) thread return 42 # Return specific valueUse with caution — skips cleanup code, can leave state inconsistent.
---
Part 4: Expression Evaluation
Swift Expressions
(lldb) expr let x = 42; print(x)
(lldb) expr self.view.backgroundColor = UIColor.red
(lldb) expr UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap { $0.windows }.first { $0.isKeyWindow }?.rootViewController
(lldb) expr UserDefaults.standard.set(true, forKey: "debug")Objective-C Expressions
Switch to ObjC when Swift expression parser fails:
(lldb) expr -l objc -- (void)[CATransaction flush]
(lldb) expr -l objc -- (id)[[[[UIApplication sharedApplication] connectedScenes] anyObject] keyWindow]
(lldb) expr -l objc -- (void)[[NSNotificationCenter defaultCenter] postNotificationName:@"test" object:nil]UI Debugging Expressions
(lldb) expr -l objc -- (void)[[[[[UIApplication sharedApplication] connectedScenes] anyObject] keyWindow] recursiveDescription]
(lldb) po UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.flatMap { $0.windows }.first { $0.isKeyWindow }?.rootViewController?.view.recursiveDescription()SwiftUI Debugging
(lldb) expr Self._printChanges() # Print what triggered body re-eval (inside view body only)Runtime Type Information
(lldb) expr type(of: someValue)
(lldb) expr String(describing: type(of: someValue))---
Part 5: Process Control
Execution Control
(lldb) continue # Resume execution (c)
(lldb) c # Short form
(lldb) process interrupt # Pause running process
(lldb) thread step-over # Step over (n / next)
(lldb) n # Short form
(lldb) thread step-in # Step into (s / step)
(lldb) s # Short form
(lldb) thread step-out # Step out (finish)
(lldb) finish # Short form
(lldb) thread step-inst # Step one instruction (assembly-level)
(lldb) ni # Step over one instructionProcess Management
(lldb) process launch # Launch/restart
(lldb) process attach --pid 1234 # Attach to running process
(lldb) process attach --name MyApp # Attach by name
(lldb) process detach # Detach without killing
(lldb) kill # Kill debugged process---
Part 6: Memory & Image
Memory Reading
(lldb) memory read 0x100abc123 # Read memory at address
(lldb) memory read -c 64 0x100abc123 # Read 64 bytes
(lldb) memory read -f x 0x100abc123 # Format as hex
(lldb) memory read -f s 0x100abc123 # Format as stringMemory Search
(lldb) memory find -s "searchString" -- 0x100000000 0x200000000Image/Module Inspection
(lldb) image lookup -a 0x100abc123 # Lookup symbol at address
(lldb) image lookup -n myFunction # Find function by name
(lldb) image lookup -rn "MyClass.*" # Regex search
(lldb) image list # List all loaded images/frameworks
(lldb) image list -b # Brief formatCommon use: Finding which framework a crash address belongs to:
(lldb) image lookup -a 0x1a2b3c4d5---
Part 7: .lldbinit & Customization
File Location
LLDB reads ~/.lldbinit at startup. Per-project init files are also supported when configured in Xcode's scheme settings.
Useful Aliases
Add to ~/.lldbinit:
# Quick reload — flush UI changes made via expression
command alias flush expr -l objc -- (void)[CATransaction flush]
# Print view hierarchy (scene-aware key window — UIApplication.keyWindow is deprecated since iOS 13)
command alias views expr -l objc -- (void)[[[[[UIApplication sharedApplication] connectedScenes] anyObject] keyWindow] recursiveDescription]
# Print auto layout constraints (UIWindowScene.keyWindow, iOS 15+)
command alias constraints po [[[[[UIApplication sharedApplication] connectedScenes] anyObject] keyWindow] _autolayoutTrace]Custom Type Summaries
# Show CLLocationCoordinate2D as "lat, lon"
type summary add CLLocationCoordinate2D --summary-string "${var.latitude}, ${var.longitude}"Settings
(lldb) settings show target.language # Current language
(lldb) settings set target.language swift # Force Swift mode
(lldb) settings set target.max-children-count 100 # Show more collection itemsPer-Project .lldbinit
In Xcode: Edit Scheme → Run → Options → "LLDB Init File" field.
Put project-specific aliases and breakpoints in a .lldbinit file in your project root.
---
Part 8: Troubleshooting LLDB Itself
"expression failed to parse"
Cause: Swift expression parser can't resolve types from the current module.
Fixes: 1. Use v instead (no compilation needed) 2. Simplify the expression 3. Try expr -l objc -- ... for ObjC-bridge types 4. Clean derived data and rebuild
"variable not available"
Cause: Compiler optimized the variable out.
Fixes: 1. Switch to Debug build configuration 2. Set -Onone for the specific file (Build Settings → per-file compiler flags) 3. Use register read to check if the value is in a register
"wrong language mode"
Cause: LLDB defaults to ObjC in some contexts (especially in frameworks).
Fix:
(lldb) settings set target.language swift
(lldb) expr -l swift -- mySwiftExpression"expression caused a crash"
Cause: The expression you evaluated had a side effect that crashed.
Fix: 1. Don't evaluate expressions that modify state unless you intend to 2. Use v for read-only inspection 3. If the crash corrupted state, restart the debug session
LLDB Hangs or Is Slow
Cause: Usually compiling a complex expression or resolving types in a large project.
Fix: 1. Use v instead of p/po (no compilation) 2. Reduce expression complexity 3. If LLDB hangs during po, Ctrl+C to cancel and use v instead
Breakpoint Not Hit
Causes and fixes:
| Cause | Fix |
|---|---|
| Wrong file/line (code moved) | Re-set breakpoint on current code |
| Breakpoint disabled | breakpoint enable N |
| Code not executed | Verify the code path is reached |
| Optimized out (Release) | Switch to Debug configuration |
| In a framework/SPM package | Set symbolic breakpoint by function name |
---
Resources
WWDC: 2019-429, 2018-412, 2022-110370, 2015-402
Docs: /xcode/stepping-through-code-and-inspecting-variables-to-isolate-bugs, /xcode/setting-breakpoints-to-pause-your-running-app
Skills: axiom-build (skills/lldb.md), axiom-build (skills/xcode-debugging.md), axiom-tools (skills/xcsym-ref.md)
LLDB Debugging
Interactive debugging with LLDB. The debugger freezes time so you can interrogate your running app — inspect variables, evaluate expressions, navigate threads, and understand exactly why something went wrong.
Core insight: "LLDB is useless" really means "I don't know which command to use for Swift types." This is a knowledge-gap problem, not a tool problem.
Red Flags — Check This Skill When
| Symptom | This Skill Applies |
|---|---|
| Need to inspect a variable at runtime | Yes — breakpoint + inspect |
| Crash you can reproduce locally | Yes — breakpoint before crash site |
| Wrong value at runtime but code looks correct | Yes — step through and inspect |
| Need to understand thread state during hang | Yes — pause + thread backtrace |
po doesn't work / shows garbage | Yes — Playbook 3 has alternatives |
| Crash log analyzed, need to reproduce | Yes — set breakpoints from crash context (symbolicate with xcsym first — see axiom-tools (skills/xcsym-ref.md)) |
| Need to test a fix without rebuilding | Yes — expression evaluation |
| Want to break on all exceptions | Yes — exception breakpoints |
| App feels slow but responsive | No — use axiom-performance (skills/performance-profiling.md) |
| Memory grows over time | No — use axiom-performance (skills/memory-debugging.md) first |
| App completely frozen | Maybe — use axiom-performance (skills/hang-diagnostics.md) first, then LLDB for thread inspection |
| Crash in production, no local repro | No — symbolicate with axiom-tools (skills/xcsym-ref.md) to get pattern_tag, then axiom-shipping (skills/testflight-triage.md) |
Have a .ips, MetricKit, or .crash text file | No — run xcsym crash first for full pipeline (parse → symbolicate → categorize) |
LLDB vs Other Tools
digraph tool_selection {
"What do you need?" [shape=diamond];
"axiom-tools (skills/xcsym-ref.md)" [shape=box];
"axiom-shipping (skills/testflight-triage.md)" [shape=box];
"axiom-performance (skills/hang-diagnostics.md)" [shape=box];
"axiom-performance (skills/memory-debugging.md)" [shape=box];
"axiom-performance (skills/performance-profiling.md)" [shape=box];
"LLDB (this skill)" [shape=box, style=bold];
"What do you need?" -> "axiom-tools (skills/xcsym-ref.md)" [label="Have a .ips, MetricKit,\nor .crash file"];
"What do you need?" -> "axiom-shipping (skills/testflight-triage.md)" [label="Field crash,\nalready symbolicated"];
"What do you need?" -> "axiom-performance (skills/hang-diagnostics.md)" [label="App frozen,\nneed diagnosis approach"];
"What do you need?" -> "axiom-performance (skills/memory-debugging.md)" [label="Memory growing,\nneed leak pattern"];
"What do you need?" -> "axiom-performance (skills/performance-profiling.md)" [label="Need to measure\nCPU/memory over time"];
"What do you need?" -> "LLDB (this skill)" [label="Need to inspect state\nat a specific moment"];
}Rule of thumb: Instruments measures. LLDB inspects. If you need to understand what's happening at a specific moment in time, use LLDB. If you need to understand trends over time, use Instruments.
Response Format
When helping with LLDB debugging, structure your output as:
1. Immediate diagnosis (1-3 bullets, confidence-tagged: HIGH/MEDIUM/LOW) 2. Commands to run (numbered, copy-paste ready, with (lldb) prefix) 3. What to look for (command → expected output → interpretation) 4. Likely root causes (ranked by probability) 5. Next breakpoint plan (catch it earlier next time) 6. If no debugger attached (crash-log-only fallback path)
---
Playbook 1: Crash Triage
Goal: Understand why the app crashed, starting from the stop point.
Step 1: Read the Stop Reason
When the debugger stops, the first thing to check:
(lldb) thread infoThis shows the stop reason. Common stop reasons:
| Stop Reason | Meaning | Next Step |
|---|---|---|
EXC_BAD_ACCESS (SIGSEGV) | Accessed invalid memory (null pointer, dangling reference) | Check the address — 0x0 to 0x10 = nil dereference |
EXC_BAD_ACCESS (SIGBUS) | Misaligned or invalid address | Usually C interop or unsafe pointer issue |
EXC_BREAKPOINT (SIGTRAP) | Hit a trap — Swift runtime check failed | Check for fatalError(), preconditionFailure(), force-unwrap of nil, array out of bounds |
EXC_CRASH (SIGABRT) | Deliberate abort — assertion or uncaught exception | Look at "Application Specific Information" for the message |
breakpoint | Your breakpoint was hit | Normal — inspect state |
Step 2: Get the Backtrace
(lldb) btRead top-to-bottom. Find the first frame in YOUR code (not system frameworks). That's where to start investigating.
(lldb) bt 10Limit to 10 frames if the full trace is noisy.
Step 3: Navigate to Your Frame
(lldb) frame select 3Jump to frame 3 (or whichever frame is in your code).
Step 4: Inspect State
(lldb) v
(lldb) v self.someProperty
(lldb) v localVariableUse v (not po) for reliable Swift value inspection. See Playbook 3 for details.
Step 5: Classify and Fix
| Exception Type | Typical Cause | Fix Pattern |
|---|---|---|
EXC_BAD_ACCESS at low address | Force-unwrap nil optional | guard let / if let |
EXC_BAD_ACCESS at high address | Use-after-free / dangling pointer | Check object lifetime, [weak self] |
EXC_BREAKPOINT | Swift runtime trap (bounds, unwrap, precondition) | Fix the violated precondition |
SIGABRT | Uncaught ObjC exception or fatalError() | Read the exception message, fix the root cause |
Step 6: Set a Conditional Breakpoint to Catch It Earlier
(lldb) breakpoint set -f MyFile.swift -l 42 -c "value == nil"This breaks only when value is nil at line 42 — catches the problem before the crash.
---
Playbook 2: Hang/Deadlock Diagnosis
Goal: Understand why the app is frozen by inspecting all thread states.
Step 1: Pause the App
If the app is hung, press the pause button in Xcode (⌃⌘Y) or:
(lldb) process interruptStep 2: Get All Thread Backtraces
(lldb) thread backtrace allOr the shorthand:
(lldb) bt allStep 3: Classify Thread States
Look at Thread 0 (main thread) — it processes all UI events. If it's blocked, the app is frozen.
Main thread blocked on synchronous wait:
frame #0: libsystem_kernel.dylib`__psynch_mutexwait
frame #1: libsystem_pthread.dylib`_pthread_mutex_firstfit_lock_wait
...
frame #5: MyApp`ViewController.viewDidLoad()Translation: Main thread is waiting for a mutex lock. Something else holds it.
Main thread blocked on dispatch_sync:
frame #0: libdispatch.dylib`_dispatch_sync_f_slow
...
frame #3: MyApp`DataManager.fetchData()Translation: DispatchQueue.main.sync called from background → classic deadlock.
Main thread busy (CPU-bound):
frame #0: MyApp`ImageProcessor.processAllImages()
frame #1: MyApp`ViewController.viewDidLoad()Translation: Expensive work on main thread. Move to background.
Step 4: Check for Deadlocks
If two threads are both waiting on something the other holds:
(lldb) thread listLook for multiple threads with state waiting that reference each other's locks.
Step 5: Inspect Specific Thread
(lldb) thread select 3
(lldb) bt
(lldb) vSwitch to another thread to inspect its state.
Cross-reference: For fix patterns once you've identified the hang cause → See axiom-performance (skills/hang-diagnostics.md)
---
Playbook 3: Swift Value Inspection
This is the core value of this skill. Most developers abandon LLDB because po doesn't work reliably with Swift types. Here's what actually works.
The Four Print Commands
| Command | Full Form | What It Does | Best For |
|---|---|---|---|
v | frame variable | Reads memory directly, no compilation | Swift structs, enums, locals — your default |
p | expression (with formatter) | Compiles expression, shows formatted result | Computed properties, function calls |
po | expression --object-description | Calls debugDescription | Classes with CustomDebugStringConvertible |
expr | expression | Evaluates arbitrary code | Calling methods, modifying state |
When to Use Each
Start with `v` — it's fastest and most reliable for stored properties:
(lldb) v self.userName
(lldb) v self.items[0]
(lldb) v localStructv works by reading memory directly. It doesn't compile anything, so it can't fail due to expression compilation errors.
`v` limitation: It only reads stored properties — computed properties, lazy var (before first access), and property wrapper projected values ($binding) won't show meaningful values. If a field looks wrong or missing with v, try p instead.
Use `p` when `v` can't reach it:
(lldb) p self.computedProperty
(lldb) p self.items.count
(lldb) p someFunction()p compiles and executes the expression. Needed for computed properties and function calls.
Use `po` for class descriptions:
(lldb) po myObject
(lldb) po error
(lldb) po notificationpo calls debugDescription on the result. Best for objects that have meaningful descriptions (NSError, Notification, etc.).
The "LLDB Is Broken" Moments
| What You See | Why | Fix |
|---|---|---|
<uninitialized> | po failed; variable hasn't been populated by optimizer | Use v instead |
expression failed to parse, unknown type name | Swift expression parser can't resolve the type | Try expr -l objc -- (id)0x12345 for ObjC objects, or use v |
<variable not available> | Compiler optimized it out (Release build) | Rebuild with Debug, per-file -Onone, or register read as last resort |
error: Couldn't apply expression side effects | Expression had side effects LLDB couldn't reverse | Try a simpler expression; avoid mutating state |
po shows memory address instead of value | Object doesn't conform to CustomDebugStringConvertible | Use v for raw value, or implement the protocol |
cannot find 'self' in scope | Breakpoint is in a context without self (static, closure) | Use v with the explicit variable name |
p shows $R0 = ... but po crashes | Different compilation paths | Use p when it works; po adds an extra description step that can fail |
Inspecting Optionals
(lldb) v optionalValueShows: (String?) some = "hello" or (String?) none
Don't use po optionalValue — it may show just Optional("hello") which is less useful.
Inspecting Collections
(lldb) v myArray
(lldb) v myArray[2]
(lldb) v myDictFor large collections, limit output:
(lldb) p Array(myArray.prefix(5))Inspecting SwiftUI State
SwiftUI @State is backed by stored properties with underscore prefix:
(lldb) v self._isPresented
(lldb) v self._itemsFor @Observable models:
(lldb) v self.viewModel.propertyNameDiagnosing "view doesn't update": If a property changes (confirmed with v) but the SwiftUI view doesn't re-render, check which thread the mutation happens on with bt. @Observable mutations must happen on @MainActor for SwiftUI to observe them — mutations on a background actor won't trigger view updates. Use Self._printChanges() inside a view body to see which property triggered (or didn't trigger) a re-render:
(lldb) expr Self._printChanges()For the full observation diagnostic tree → /skill axiom-swiftui (debugging reference)
Inspecting Actors
Actor state is best inspected with v, which reads memory directly without isolation concerns:
(lldb) v actorShows all stored properties. This works because LLDB pauses the entire process — you can read any memory regardless of actor isolation (which is a compile-time concept).
Modifying Values at Runtime
(lldb) expr self.debugFlag = true
(lldb) expr myArray.append("test")
(lldb) expr self.view.backgroundColor = UIColor.redModify values without rebuilding. Useful for testing theories.
Triggering Background Tasks (BGTaskScheduler)
Background tasks never launch on their own in the simulator. With the app paused in the debugger, drive the handler by hand:
(lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.yourapp.refresh"]
(lldb) e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateExpirationForTaskWithIdentifier:@"com.yourapp.refresh"]Set a breakpoint in the task handler first, run the launch command, then continue. See axiom-integration (skills/background-processing.md) for the full app-refresh testing workflow.
Referencing Previous Results
LLDB assigns result variables ($R0, $R1, etc.):
(lldb) p someValue
$R0 = 42
(lldb) p $R0 + 10
$R1 = 52---
Playbook 4: Breakpoint Strategies
Source Breakpoints (Basic)
(lldb) breakpoint set -f ViewController.swift -l 42
(lldb) b ViewController.swift:42Short form b works for simple cases.
Conditional Breakpoints
Break only when a condition is true:
(lldb) breakpoint set -f MyFile.swift -l 42 -c "index > 100"
(lldb) breakpoint set -f MyFile.swift -l 42 -c "name == \"test\""Iteration-based: Break after N hits:
(lldb) breakpoint set -f MyFile.swift -l 42 -i 50Ignores the first 50 hits, then breaks.
Logpoints (Action + Auto-Continue)
Log without stopping — like a print statement but no rebuild needed:
(lldb) breakpoint set -f MyFile.swift -l 42
(lldb) breakpoint command add 1
> v self.value
> continue
> DONEOr in Xcode: Edit breakpoint → Add Action → "Log Message" → use @self.value@ token syntax → Check "Automatically continue"
Symbolic Breakpoints
Break on ANY call to a method by name:
(lldb) breakpoint set -n viewDidLoad
(lldb) breakpoint set -n "MyClass.myMethod"Break on all ObjC messages to a selector:
(lldb) breakpoint set -S "layoutSubviews"Exception Breakpoints
Swift errors (break on throw):
(lldb) breakpoint set -E swiftObjective-C exceptions (break on throw):
(lldb) breakpoint set -E objcIn Xcode: Breakpoint Navigator → + → Swift Error Breakpoint / Exception Breakpoint
This is the single most useful breakpoint for crash debugging. It stops at the throw site instead of the catch/crash site.
Watchpoints
Break when a variable's value changes:
(lldb) watchpoint set variable self.count
(lldb) watchpoint set variable -w read_write myGlobalWatchpoints are hardware-backed — limited to ~4 per process but very fast.
One-Shot Breakpoints
Break once, then auto-delete:
(lldb) breakpoint set -f MyFile.swift -l 42 -oManaging Breakpoints
(lldb) breakpoint list
(lldb) breakpoint disable 3
(lldb) breakpoint enable 3
(lldb) breakpoint delete 3
(lldb) breakpoint delete---
Playbook 5: Async/Concurrency Debugging
Identifying Async Frames
Swift concurrency backtraces are noisy — expect swift_task_switch, _dispatch_call_block_and_release, and executor internals mixed in with your code. Don't be discouraged by 40+ frames of runtime noise. Focus on frames from YOUR module.
In Swift concurrency backtraces, look for swift-task frames:
Thread 3:
frame #0: MyApp`MyActor.doWork()
frame #1: swift_task_switch
frame #2: MyApp`closure #1 in ViewController.loadData()The swift_task_switch frame indicates an async suspension point. Your code frames are the ones prefixed with your module name (MyApp above).
Inspecting Task State
(lldb) thread backtrace allLook for threads with swift_task in their frames. Each represents an active Swift task.
Actor-Isolated Code
When stopped inside an actor:
(lldb) v selfShows all actor state. This works because LLDB pauses the entire process — actor isolation is a compile-time concept, not a runtime lock (for default actors).
Task Group Inspection
When debugging task groups, break inside the group closure and inspect:
(lldb) v
(lldb) btEach child task runs on its own thread. Use bt all to see them.
Cross-reference: For Swift concurrency patterns and fix strategies → /skill axiom-concurrency. For profiling async performance → /skill axiom-concurrency
---
Pressure Scenarios
Scenario 1: "Release-Only Crash — LLDB Is Useless in Release"
Situation: Crash happens in Release builds but not Debug. Team says "we can't debug it."
Why this fails: Release optimizations change timing, memory layout, and can eliminate variables — making the crash non-reproducible in Debug.
Correct approach:
1. Build with Debug configuration but Release-like settings:
- Optimization Level:
-O(not-Onone) - Still include debug symbols (
DEBUG_INFORMATION_FORMAT = dwarf-with-dsym)
2. Enable Address Sanitizer (-fsanitize=address) — catches memory errors with 2-3x overhead 3. Use the crash report to set breakpoints at the crash site 4. Set exception breakpoints to catch the error before the crash:
(lldb) breakpoint set -E swift
(lldb) breakpoint set -E objc5. If variable shows <optimized out>, reduce optimization for that one file:
- Build Settings → Per-file flags →
-Ononefor the specific file
6. Last resort — read register values directly (variables live in registers before being optimized out):
(lldb) register read
(lldb) register read x0 x1 x2On ARM64: x0 = self, x1-x7 = first 7 arguments. Check See axiom-build (skills/lldb-ref.md) Part 1 for details.
Scenario 2: "Just Add Print Statements"
Situation: Developer adds print() calls to debug, rebuilds, runs, reads console. Repeat.
Why this fails: Each print-debug cycle costs 3-5 minutes (edit → build → run → navigate to state → read output). An LLDB breakpoint costs 30 seconds.
Correct approach:
1. Set a breakpoint at the line you'd add a print():
(lldb) b MyFile.swift:422. Add a logpoint for "print-like" behavior without rebuilding:
- Edit breakpoint → Add Action → Log Message → Check "Auto continue"
3. Inspect variables directly: v self.someValue 4. Modify variables at runtime to test theories: expr self.debugMode = true 5. One breakpoint session replaces 5-10 print-debug cycles.
Time comparison (typical control-flow debugging):
| Approach | Per investigation | 5 variables |
|---|---|---|
| print() statements | 3-5 min (build + run) | 15-25 min |
| LLDB breakpoint | 30 sec (set + inspect) | 2.5 min |
Exception: In tight loops (thousands of hits/sec), logpoints add per-hit overhead. Use -i to skip to the iteration you care about, or use a temporary print() for that specific loop.
Scenario 3: "po Doesn't Work So LLDB Is Broken"
Situation: Developer types po myStruct and gets garbage. Concludes LLDB is broken for Swift. Goes back to print debugging.
This is the #1 reason developers abandon LLDB.
Why `po` fails with Swift structs: po calls debugDescription which requires compiling an expression in the debugger context. For Swift structs, this compilation often fails due to missing type metadata, generics, or module resolution issues.
Correct approach:
1. Use v instead of po — reads memory directly, no compilation:
(lldb) v myStruct
(lldb) v myStruct.propertyName2. Use p for computed properties:
(lldb) p myStruct.computedValue3. Use po only for classes with CustomDebugStringConvertible 4. If p also fails, try specifying the language:
(lldb) expr -l objc -- (id)0x123455. If everything fails, v self always works inside a method.
---
Anti-Patterns
| Anti-Pattern | Why It's Wrong | Better Alternative |
|---|---|---|
po everything | Fails for Swift structs, enums, optionals | v for values, po only for classes |
| Print-debug cycles | 3-5 min per cycle vs 30 sec breakpoint | Breakpoints with logpoint actions |
| "LLDB doesn't work with Swift" | It does — wrong command choice | v is designed for Swift values |
| Ignoring backtraces | Jumping to guesses instead of reading the trace | bt first, then navigate frames |
| Conditional breakpoints on every hit | Slows execution if condition is expensive | Use -i (ignore count) when possible |
| Debugging optimized (Release) builds | Variables missing, code reordered | Debug configuration, or per-file -Onone |
| Force-continuing past exceptions | Hides the real error | Fix the exception, don't suppress it |
| No exception breakpoints set | Crashes land in system code, not throw site | Always add Swift Error + ObjC Exception breakpoints |
Debugging Checklist
Before starting a debug session:
- [ ] Debug build configuration (not Release)
- [ ] Exception breakpoints enabled (Swift Error + ObjC Exception)
- [ ] Breakpoint set before suspected problem area
- [ ] Know which command to use:
vfor values,pfor computed,pofor descriptions
During debug session:
- [ ] Read stop reason (
thread info) before anything else - [ ] Get backtrace (
bt) — find your frame - [ ] Navigate to your frame (
frame select N) - [ ] Inspect relevant state (
v self,v localVar) - [ ] Understand the cause before writing any fix
After finding the issue:
- [ ] Set conditional breakpoint to catch recurrence
- [ ] Consider adding assertion/precondition for this case
- [ ] Remove temporary breakpoints
Resources
WWDC: 2019-429, 2018-412, 2022-110370
Docs: /xcode/stepping-through-code-and-inspecting-variables-to-isolate-bugs, /xcode/setting-breakpoints-to-pause-your-running-app, /xcode/diagnosing-memory-thread-and-crash-issues-early
Skills: axiom-build (skills/lldb-ref.md), axiom-tools (skills/xcsym-ref.md), axiom-shipping (skills/testflight-triage.md), axiom-performance (skills/hang-diagnostics.md), axiom-performance (skills/memory-debugging.md), axiom-integration (skills/background-processing.md), axiom-concurrency
Xcode Debugging
Overview
Check build environment BEFORE debugging code. Core principle 80% of "mysterious" Xcode issues are environment problems (stale Derived Data, stuck simulators, zombie processes), not code bugs.
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "My build is failing with 'BUILD FAILED' but no error details. I haven't changed anything. What's going on?"
→ The skill shows environment-first diagnostics: check Derived Data, simulator states, and zombie processes before investigating code
2. "Tests passed yesterday with no code changes, but now they're failing. This is frustrating. How do I fix this?"
→ The skill explains stale Derived Data and intermittent failures, shows the 2-5 minute fix (clean Derived Data)
3. "My app builds fine but it's running the old code from before my changes. I restarted Xcode but it still happens."
→ The skill demonstrates that Derived Data caches old builds, shows how deletion forces a clean rebuild
4. "The simulator says 'Unable to boot simulator' and I can't run tests. How do I recover?"
→ The skill covers simulator state diagnosis with simctl and safe recovery patterns (erase/shutdown/reboot)
5. "I'm getting 'No such module: SomePackage' errors after updating SPM dependencies. How do I fix this?"
→ The skill explains SPM caching issues and the clean Derived Data workflow that resolves "phantom" module errors
---
Red Flags — Check Environment First
If you see ANY of these, suspect environment not code:
- "It works on my machine but not CI"
- "Tests passed yesterday, failing today with no code changes"
- "Build succeeds but old code executes"
- "Build sometimes succeeds, sometimes fails" (intermittent failures)
- "Simulator stuck at splash screen" or "Unable to install app"
- Multiple xcodebuild processes (10+) older than 30 minutes
Mandatory First Steps
ALWAYS run these commands FIRST (before reading code):
# 1. Check processes (zombie xcodebuild?)
# \bxcodebuild\b is word-bounded so it skips the `xcodebuildmcp` MCP server
ps aux | grep -E '\bxcodebuild\b|Simulator' | grep -v grep
# 2. Check Derived Data size (>10GB = stale)
du -sh ~/Library/Developer/Xcode/DerivedData
# 3. Check simulator states (stuck Booting?)
xcrun simctl list devices | grep -E "Booted|Booting|Shutting Down"What these tell you
- 0 processes + small Derived Data + no booted sims → Environment clean, investigate code
- 10+ processes OR >10GB Derived Data OR simulators stuck → Environment problem, clean first
- Stale code executing OR intermittent failures → Clean Derived Data regardless of size
Why environment first
- Environment cleanup: 2-5 minutes → problem solved
- Code debugging for environment issues: 30-120 minutes → wasted time
Quick Fix Workflow
Finding Your Scheme Name
If you don't know your scheme name:
# List available schemes
xcodebuild -listFor Stale Builds / "No such module" Errors
# Clean everything
xcodebuild clean -scheme YourScheme
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf .build/ build/
# Rebuild
xcodebuild build -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16'For Simulator Issues
# Shutdown all simulators
xcrun simctl shutdown all
# If simctl command fails, shutdown and retry
xcrun simctl shutdown all
xcrun simctl list devices
# If still stuck, erase specific simulator
xcrun simctl erase <device-uuid>
# Nuclear option: force-quit Simulator.app
killall -9 SimulatorFor Zombie Processes
# Kill all xcodebuild (use cautiously)
killall -9 xcodebuild
# Check they're gone (-w skips the `xcodebuildmcp` MCP server)
ps aux | grep -w xcodebuild | grep -v grepFor Test Failures
# Isolate failing test
xcodebuild test -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:YourTests/SpecificTestClassSimulator Verification (Optional)
After applying fixes, verify in simulator with visual confirmation.
Quick Screenshot Verification
# 1. Boot simulator (if not already)
xcrun simctl boot "iPhone 16 Pro"
# 2. Build and install app
xcodebuild build -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16 Pro'
# 3. Launch app
xcrun simctl launch booted com.your.bundleid
# 4. Wait for UI to stabilize
sleep 2
# 5. Capture screenshot
xcrun simctl io booted screenshot /tmp/verify-build-$(date +%s).pngUsing Axiom Tools
Quick screenshot:
/axiom:screenshotFull simulator testing (with navigation, state setup):
/axiom:test-simulatorWhen to Use Simulator Verification
Use when:
- Visual fixes — Layout changes, UI updates, styling tweaks
- State-dependent bugs — "Only happens in this specific screen"
- Intermittent failures — Need to reproduce specific conditions
- Before shipping — Final verification that fix actually works
Pro tip: If you have debug deep links (see axiom-swift (skills/deep-link-debugging.md) skill), you can navigate directly to the screen that was broken:
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
xcrun simctl io booted screenshot /tmp/fix-verification.pngDecision Tree
Test/build failing?
├─ BUILD FAILED with no details?
│ └─ Clean Derived Data → rebuild
├─ Build intermittent (sometimes succeeds/fails)?
│ └─ Clean Derived Data → rebuild
├─ Build succeeds but old code executes?
│ └─ Delete Derived Data → rebuild (2-5 min fix)
├─ "Unable to boot simulator"?
│ └─ xcrun simctl shutdown all → erase simulator
├─ "No such module PackageName"?
│ └─ Clean + delete Derived Data → rebuild
├─ Tests hang indefinitely?
│ └─ Check simctl list → reboot simulator
├─ Tests crash?
│ └─ Check ~/Library/Logs/DiagnosticReports/*.crash
└─ Code logic bug?
└─ Use systematic-debugging skill insteadCommon 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 |
Predicted vs. built issues (OS27): Xcode 27 surfaces predicted issues inline before you build, rendered with a subtle, theme-blended style. They firm up into full-color warnings/errors when you build — or vanish if already resolved. A predicted issue is not yet a confirmed build failure: build (or check the build log) before treating an inline marker as real, so environment-first triage stays honest.
Useful CLI Tools
# Show build settings
xcodebuild -showBuildSettings -scheme YourScheme
# List schemes/targets
xcodebuild -list
# Verbose output
xcodebuild -verbose build -scheme YourScheme
# Build without testing (faster)
xcodebuild build-for-testing -scheme YourScheme
xcodebuild test-without-building -scheme YourScheme
# Version and build number management (agvtool)
xcrun agvtool what-marketing-version # Current version (e.g., 2.0)
xcrun agvtool what-version # Current build number
xcrun agvtool next-version -all # Bump build number
xcrun agvtool new-version -all 42 # Set specific build number
xcrun agvtool new-marketing-version 2.1 # Set marketing version
# Validate asset catalogs (actool surfaces warnings during compile — no bare "lint" subcommand)
xcrun actool Assets.xcassets --compile /tmp/actool-out \
--platform iphoneos --minimum-deployment-target 26.0 \
--app-icon AppIcon --output-partial-info-plist /tmp/partial.plistxcsym crash <file>— Structured crash symbolication with LLM-friendly JSON output. Use for any.ips, MetricKit, or legacy.crashtext file. Seeaxiom-tools (skills/xcsym-ref.md).
Device Management (devicectl)
devicectl is the modern Core Device CLI (Xcode 15+, replaces legacy idevice* tools) for installing, launching, inspecting, and managing devices from the command line, with --json-output for CI.
xcrun devicectl list devices returns a *unified inventory of physical devices and simulators, distinguished by a `Reality` column (`physical` / `simulated`) — the CLI counterpart to Device Hub (below). This is not* new in Xcode 27: the devicectl CLI is byte-identical between Xcode 26 and 27 (binary 629.3 in both, same 85 subcommands and flags). Xcode 27's one devicectl-related change is service-side — per the release notes, simctl and devicectl now support rebooting a simulator via reboot. For richer simulator-only control (status bar, push, privacy permissions, media), simctl stays primary; to drive the simulator UI / accessibility tree, use the Axiom xcui tool (axiom-tools (skills/xcui-ref.md)).
# Unified inventory: physical + simulated (--json-output for CI)
xcrun devicectl list devices
# Install / launch / inspect a physical device by identifier
xcrun devicectl device install app --device <udid> MyApp.app
xcrun devicectl device process launch --device <udid> com.your.bundleid
xcrun devicectl device info apps --device <udid>
xcrun devicectl device info processes --device <udid>When to use: CLI device operations when an issue doesn't reproduce in Simulator (install, launch, inspect) — and list devices as the single command that inventories devices and simulators together.
Parsing `list devices --json-output`: there is no literal simulated field — the human-readable Reality column is derived from connectionProperties.transportType (sameMachine = simulator; localNetwork / wired = physical device). Key off that, alongside deviceProperties.bootState (booted / shutdown) and hardwareProperties.deviceType, when enumerating devices and simulators in CI.
Device Hub (OS27)
Xcode 27 unifies simulators and physical devices in Device Hub — a standalone app that ships alongside Xcode and auto-launches when you build and run to a simulator (you don't need to open Xcode to use it). It offers the same toolset for simulators and physical devices, in a compact window (live screen plus a few essentials) that expands to a full window with canvas, sidebar inventory, and inspector. Bottom controls are contextual — home/screenshot/rotate on iPhone, play/pause and navigation on Apple TV, environment/camera on Vision Pro, side button and Digital Crown on Apple Watch.
The canvas is a live, interactive screen (click, drag, scroll, trackpad gestures) for a device or simulator, with zoom, snap-to-1:1 physical size, Resize mode (transform app dimensions freely — see axiom-uikit for resizability), and Capture keyboard (routes Mac keystrokes to the device for key-command and hardware testing).
Inspector panels
Five panels; two carry most of the debugging weight — Diagnostic reports (investigate) and Device settings (reproduce conditions).
| Panel | Use |
|---|---|
| Device settings | Appearance and accessibility applied instantly — dark mode, increased contrast, larger Dynamic Type, simulated location, audio (no digging through Settings) |
| Diagnostic reports | Start here when the app hangs or crashes — crashes, spins, and other logged diagnostics |
| Info | Storage, model, serial number |
| Apps | Install/uninstall; download and replace data containers |
| Profiles | Configuration and provisioning profiles |
Reproduce a device-only bug on a simulator
The canonical Device Hub workflow when a bug reproduces on a physical device but not locally:
1. Capture from the device — Pair Nearby Device (wireless), install any needed configuration profile (e.g. a CoreLocation logging profile; reboot for privacy), reproduce the bug, then screenshot it, run a sysdiagnose for system-level diagnostics, and download the app's data container. 2. Match on the simulator — select the matching model, replace your data container with the device's (Apps panel), then mirror the triggering config: rotation, simulated location, Dynamic Type size.
Device-only bugs often need a confluence of conditions (e.g. landscape + a specific location + max text size, all at once); the inspector lets you reproduce every one of them in a single place.
Automation and CI
simctl (simulators) and devicectl (devices — its list devices also inventories simulators via the Reality column) remain the scriptable path — Device Hub is a GUI over the same operations, not a replacement. devicectl lists devices, installs apps, changes settings (e.g. dark/light mode), and queries device info, with --json-output for clean integration into scripts and CI. Reach for the CLI in scripts, CI, and headless verification — and the Axiom xcui tool for driving the simulator UI (see axiom-tools (skills/xcui-ref.md)). On macOS27 the iPhone Mirroring window is resizable.
Crash Log Analysis
# Recent crashes
ls -lt ~/Library/Logs/DiagnosticReports/*.crash | head -5
# Symbolicate a single address (if you have .dSYM)
xcrun atos -o YourApp.app.dSYM/Contents/Resources/DWARF/YourApp \
-arch arm64 -l 0x100000000 0x<address>
# Symbolicate an entire crash log at once (LLDB Python script, may vary by Xcode version)
xcrun crashlog MyCrash.ipsCommon Mistakes
❌ Debugging code before checking environment — Always run mandatory steps first
❌ Ignoring simulator states — "Booting" can hang 10+ minutes, shutdown/reboot immediately
❌ Assuming git changes caused the problem — Derived Data caches old builds despite code changes
❌ Running full test suite when one test fails — Use -only-testing to isolate
Real-World Impact
Before 30+ min debugging "why is old code running" After 2 min environment check → clean Derived Data → problem solved
Key insight Check environment first, debug code second.
Related skills
How it compares
Choose axiom-build when the blocker is Xcode compilation and archiving rather than cross-platform JavaScript bundling or backend API work.
FAQ
What does axiom-build automate in Xcode?
axiom-build automates compile, scheme-select, and archive steps for Xcode projects using xcodebuild or Xcode. The skill resolves signing, targets, Swift packages, and multi-platform build settings for iOS and macOS.
When should developers use axiom-build?
Developers use axiom-build when native iOS or macOS builds fail on signing, scheme selection, or Swift Package Manager configuration and an agent should drive xcodebuild to a successful archive.