SKILL.md
---
name: axiom-build
description: Use when ANY iOS build fails, test crashes, Xcode misbehaves, or environment issue occurs before debugging code. Covers build failures, compilation errors, dependency conflicts, simulator problems, environment-first diagnostics.
license: MIT
---
# Build & Environment
**You MUST use this skill for ANY build, environment, or Xcode-related issue before debugging application code.**
<!-- AXIOM_AUDITOR_INLINE_BEGIN — auto-maintained by scripts/build-inlined-auditors.ts; do not hand-edit -->
> **Not on Claude Code?** Where this router says "Launch `some-auditor` agent", read that auditor's file in this suite and follow it inline — the same procedure, needing only file search and read.
>
> Available here: `skills/modernization-helper.md`.
> Homed in another suite: `axiom-security/skills/security-privacy-scanner.md`.
>
> Agents that need Bash — builds, tests, simulators, crash symbolication — stay Claude Code-only; there is no inline equivalent for those.
<!-- AXIOM_AUDITOR_INLINE_END -->
## 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 FAILED` without obvious code cause
- Tests crash in clean project
- Simulator hangs or won't boot
- "No such module" after SPM changes
- Zombie `xcodebuild` processes
- 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`
skills/build-debugging.md
# 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 below
```
## Common Build Issues
### Issue 1: SPM Package Not Found
**Symptom**: "No such module PackageName" after adding Swift Package
**❌ WRONG**:
```bash
# Rebuilding without cleaning
xcodebuild build
```
**✅ CORRECT**:
```bash
# 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 YourScheme
```
### Issue 2: CocoaPods Conflicts
**Symptom**: Pod install succeeds but build fails with framework errors
**Check Podfile.lock**:
```bash
# See what versions were actually installed
cat Podfile.lock | grep -A 2 "PODS:"
# Compare with Podfile requirements
cat Podfile | grep "pod "
```
**Fix version conflicts**:
```ruby
# Podfile - be explicit about versions
pod 'Alamofire', '~> 5.8.0' # Not just 'Alamofire'
pod 'SwiftyJSON', '5.0.1' # Exact version if needed
```
**Clean reinstall**:
```bash
# Remove all pods
rm -rf Pods/
rm Podfile.lock
# Reinstall
pod install
# Open workspace (not project!)
open YourApp.xcworkspace
```
### Issue 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**:
```bash
# Show all build settings
xcodebuild -showBuildSettings -scheme YourScheme | grep FRAMEWORK_SEARCH_PATHS
```
**Fix 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**:
```bash
# In project directory
swift package show-dependencies
# Or see resolved versions
cat Package.resolved
```
**Fix conflicts**:
```swift
// 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")) // SemVer
```
**Reset resolution**:
```bash
# Clear package caches
rm -rf .build
rm Package.resolved
# Re-resolve
swift package resolve
```
## Dependency Resolution Strategies
### Strategy 1: Lock to Specific Versions
When stability matters more than latest features:
**CocoaPods**:
```ruby
pod 'Alamofire', '5.8.0' # Exact version
pod 'SwiftyJSON', '~> 5.0.0' # Any 5.0.x
```
**SPM**:
```swift
.package(url: "...", exact: "1.2.3")
```
### Strategy 2: Use Version Ranges
When you want bug fixes but not breaking changes:
**CocoaPods**:
```ruby
pod 'Alamofire', '~> 5.8' # 5.8.x but not 5.9
pod 'SwiftyJSON', '>= 5.0', '< 6.0' # Range
```
**SPM**:
```swift
.package(url: "...", from: "1.2.0") // 1.2.0 and higher
.package(url: "...", .upToNextMajor(from: "1.0.0")) // 1.x.x but not 2.0.0
```
### Strategy 3: Fork and Pin
When you need custom modifications:
```bash
# 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)**:
```swift
// Instead of this:
.package(url: "https://github.com/problematic/package")
// Fork it and remove the conflicting dependency from its Package.swift
```
**CocoaPods**:
```ruby
# 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**:
```bash
# Compare Debug and Release settings
xcodebuild -showBuildSettings -configuration Debug > debug.txt
xcodebuild -showBuildSettings -configuration Release > release.txt
diff debug.txt release.txt
```
**Common 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**:
```bash
# ❌ WRONG
open YourApp.xcodeproj
# ✅ CORRECT
open YourApp.xcworkspace
```
**Check which you're building**:
```bash
# For workspace
xcodebuild -workspace YourApp.xcworkspace -scheme YourScheme build
# For project only (no CocoaPods)
xcodebuild -project YourApp.xcodeproj -scheme YourScheme build
```
## Pressure 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: Low
```
#### Step 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
```bash
# ❌ BAD: .gitignore includes lockfiles
Podfile.lock
Package.resolved
```
**Why**: Team members get different versions, builds differ
### ❌ Using "Latest" Version
```ruby
# ❌ 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
```bash
# ❌ 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
```bash
# 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 settings
```
## Real-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
skills/build-performance.md
# 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:
```bash
/axiom:optimize-build
```
The 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.
```bash
# 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 Summary
```
**Record**:
- 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 work
```
**Long horizontal bars**: Slow individual tasks
```
Task A: ████████████████████ (45 seconds) ← Investigate
Task B: ███ (3 seconds) ← Fine
```
**Serial 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:
```swift
// Add to Debug build settings → Other Swift Flags
-warn-long-function-bodies 100
-warn-long-expression-type-checking 100
```
Build → 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**:
```swift
// ❌ 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.
#### SwiftUI nested containers `OS27`
Xcode 27's SDK restructures shared SwiftUI containers (`Group`, `Section`, `ForEach`) around `ContentBuilder` inits — `ContentBuilder` is a typealias for `ViewBuilder`; Group's inits collapse to one unconstrained `init(@ContentBuilder content:)` — plus the parameter-pack type `TupleContent`. Its domain identity (View, ToolbarContent, CustomizableToolbarContent, Commands, AccessibilityRotorContent, SceneAccessoryContent; Charts adds ChartContent) is resolved by conditional conformance after construction, instead of per-domain init overloads at every nesting level. Scene, Tab, and Table are not unified (SceneBuilder, TabContentBuilder, TableRowBuilder remain dedicated).
Measured impact (`swiftc -typecheck`):
| Nest | Xcode 26 | Xcode 27, iOS 26 target | Xcode 27, iOS 27 target |
|---|---|---|---|
| `Section`/`Group`/`ForEach`, mixed content | ~7.2s | ~1.7s | ~1.5s |
| 11-level `Group` of `Text` | ~2.4s | ~1.8s | ~0.15s |
Two mechanisms with different availability:
- The unified container inits are emitted into the client (iOS 13+), so the big win on mixed nests — the class that escalates to "unable to type-check this expression in reasonable time" errors — arrives with Xcode 27 at any deployment target (Apple: "whether you're targeting the 2027 releases, or previous releases as well").
- The unconstrained `ViewBuilder.buildBlock` returning `TupleContent` is `@available(iOS 27, *)`, so builder-path-dominated nests (homogeneous row above) collapse fully only at 27 deployment targets.
Consequences:
- Manually flattening deeply nested `Group`/`Section`/`ForEach` purely for type-check speed is largely obsolete on Xcode 27; the residual case is builder-path-dominated deep nests below a 27 deployment target (homogeneous row above). Pattern 1's explicit-type-annotation advice is unaffected — unification does nothing for chained-expression inference.
- Some pre-27 ambiguities are fixed outright: `let g = Group {}` fails to compile on Xcode 26 ("ambiguous use of init(content:)"), compiles on 27.
- Source breakage is possible because the unified builder no longer pins a domain. Apple documents the shapes that can break, and their fixes, in TN3211: non-closure `.background(...)`/`.overlay(...)` with modified `ShapeStyle` expressions ("ambiguous use of 'opacity'") → use the closure form; cross-module name collisions → qualify with the module (`SwiftUI.Color.clear`); generic constraints naming `TupleView` → use `TupleContent`; empty builder blocks with MapKit in scope ("requires 'EmptyMapContent' conform to 'View'") → explicit `EmptyView()`; deeply branching `Chart` conditionals when back-deploying → extract into `@ChartContentBuilder` functions. TN3211 also covers separate `@State`-macro incompatibilities, out of scope here.
---
### 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
```bash
# ❌ 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 build
```
**Script Phase Sandboxing** (Xcode 14+)
Enable to prevent data races and improve parallelization:
```
Build Settings → User Script Sandboxing → YES
```
**Why**: Forces you to declare inputs/outputs explicitly, enabling parallel execution.
```bash
# 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 safely
```
**Parallel 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**:
```bash
# In project.pbxproj
grep "SWIFT_COMPILATION_MODE" project.pbxproj
```
**Optimal configuration**:
| Configuration | Setting | Why |
|---|---|---|
| **Debug** | `singlefile` (Incremental) | Only recompiles changed files |
| **Release** | `wholemodule` | Maximum optimization |
```swift
// ❌ 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**:
```bash
grep "ONLY_ACTIVE_ARCH" project.pbxproj
```
**Fix**:
| 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 |
```bash
# Check current
grep "DEBUG_INFORMATION_FORMAT" project.pbxproj
```
**How 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 ──→ Utilities
```
**Timeline (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 finish
```
**After (Xcode 14+)**:
```
Framework: Compile ████████████
Emit Module ███
App: ░░░░░░███████████
↑
Starts as soon as module emitted
```
**Automatic**: 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 → YES
```
**How 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**:
```bash
# Build with caching enabled
xcodebuild build -scheme YourScheme \
COMPILATION_CACHE_ENABLE_CACHING=YES
# Check build log for cache information
```
**Current 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 cached
```
Problems:
- 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 3
```
**Common 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):
```bash
# 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 builds
```
**Expected 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):
```bash
xcodebuild clean build -scheme YourScheme 2>&1 | tee baseline.log
```
2. **Apply ONE optimization at a time**
3. **Measure improvement**:
```bash
xcodebuild clean build -scheme YourScheme 2>&1 | tee optimized.log
```
4. **Compare**:
```bash
# Extract build time from logs
grep "Build succeeded" baseline.log
grep "Build succeeded" optimized.log
```
**Example**:
```
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
```bash
# 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 -20
```
**Output**:
```
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
```bash
# 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, 2026-269
**Docs**: /xcode/improving-the-speed-of-incremental-builds, /xcode/building-your-project-with-explicit-module-dependencies, /swiftui/contentbuilder, /technotes/tn3211
**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.
skills/lldb-ref.md
# 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 = 52
```
### `po` / `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 debug
```
**Flags:**
| 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 address
```
### Conditional 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 hits
```
### One-Shot Breakpoints
```
(lldb) breakpoint set -f File.swift -l 42 -o # Delete after first hit
```
### Breakpoint Commands (Logpoints)
Add commands that execute when breakpoint hits:
```
(lldb) breakpoint command add 1
> v self.state
> p self.items.count
> continue
> DONE
```
Or 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 throw
```
### Managing 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 existing
```
### Watchpoints
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 condition
```
**Note:** 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 all
```
### Thread Navigation
```
(lldb) thread list # List all threads with state
(lldb) thread info # Current thread details + stop reason
(lldb) thread select 3 # Switch to thread 3
```
### Frame 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 frame
```
### Thread Return (Skip Code)
Force an early return from the current function:
```
(lldb) thread return # Return void
(lldb) thread return 42 # Return specific value
```
**Use 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 instruction
```
### Process 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 string
```
### Memory Search
```
(lldb) memory find -s "searchString" -- 0x100000000 0x200000000
```
### Image/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 format
```
**Common 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 items
```
### Per-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)
skills/lldb.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
```dot
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 info
```
This 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) bt
```
Read top-to-bottom. Find the first frame in YOUR code (not system frameworks). That's where to start investigating.
```
(lldb) bt 10
```
Limit to 10 frames if the full trace is noisy.
### Step 3: Navigate to Your Frame
```
(lldb) frame select 3
```
Jump to frame 3 (or whichever frame is in your code).
### Step 4: Inspect State
```
(lldb) v
(lldb) v self.someProperty
(lldb) v localVariable
```
Use `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 interrupt
```
### Step 2: Get All Thread Backtraces
```
(lldb) thread backtrace all
```
Or the shorthand:
```
(lldb) bt all
```
### Step 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 list
```
Look for multiple threads with state `waiting` that reference each other's locks.
### Step 5: Inspect Specific Thread
```
(lldb) thread select 3
(lldb) bt
(lldb) v
```
Switch 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 localStruct
```
`v` 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 notification
```
`po` 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 optionalValue
```
Shows: `(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 myDict
```
For 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._items
```
For `@Observable` models:
```
(lldb) v self.viewModel.propertyName
```
**Diagnosing "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 actor
```
Shows 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.red
```
Modify 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:42
```
Short 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 50
```
Ignores 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
> DONE
```
Or 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 swift
```
**Objective-C exceptions (break on throw):**
```
(lldb) breakpoint set -E objc
```
**In 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 myGlobal
```
Watchpoints 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 -o
```
### Managing 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 all
```
Look for threads with `swift_task` in their frames. Each represents an active Swift task.
### Actor-Isolated Code
When stopped inside an actor:
```
(lldb) v self
```
Shows 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) bt
```
Each 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 objc
```
5. If variable shows `<optimized out>`, reduce optimization for that one file:
- Build Settings → Per-file flags → `-Onone` for 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 x2
```
On 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:42
```
2. 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.propertyName
```
2. Use `p` for computed properties:
```
(lldb) p myStruct.computedValue
```
3. Use `po` only for classes with `CustomDebugStringConvertible`
4. If `p` also fails, try specifying the language:
```
(lldb) expr -l objc -- (id)0x12345
```
5. 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: `v` for values, `p` for computed, `po` for 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
skills/modernization-helper.md
<!-- GENERATED from agents/modernization-helper.md by scripts/build-inlined-auditors.ts — do not edit. -->
# Modernization Helper
**Claude Code** — launch the `modernization-helper` agent, or run `/axiom:audit modernization`. It runs this procedure in an isolated context with its own model tier.
**Every other harness** — follow this file inline. It is the same procedure, and it needs only file search and read.
You are an expert at migrating iOS apps to modern iOS 17/18+ patterns.
## Your Mission
Scan the codebase for legacy patterns and provide migration paths:
- `ObservableObject` → `@Observable`
- `@StateObject` → `@State` with Observable
- `@ObservedObject` → Direct property or `@Bindable`
- `@EnvironmentObject` → `@Environment`
- Legacy SwiftUI modifiers → Modern equivalents
- Completion handlers → async/await
## Tool Use Is Mandatory
Run every Glob, Grep, and Read this prompt lists. Do not reason from training data instead of scanning.
- Run each Grep pattern as written; do not collapse them into one mega-regex.
- Run the Read verifications each section calls for.
- "Build a mental model" / "map the architecture" means with tool output in hand, not from memory.
## Files to Scan
**Swift files**: `**/*.swift`
Skip: `*Tests.swift`, `*Previews.swift`, `*/Pods/*`, `*/Carthage/*`, `*/.build/*`, `*/DerivedData/*`, `*/scratch/*`, `*/docs/*`, `*/.claude/*`, `*/.claude-plugin/*`
## Modernization Patterns (iOS 17+ / iOS 18+)
### Pattern 1: ObservableObject → @Observable (HIGH)
**Why migrate**: Better performance (view updates only when accessed properties change), simpler syntax, no `@Published` needed
**Requirement**: iOS 17+
**Detection**:
```
Grep: class.*ObservableObject
Grep: : ObservableObject
Grep: @Published
```
```swift
// ❌ LEGACY (iOS 14-16)
class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
@Published var errorMessage: String?
}
// ✅ MODERN (iOS 17+)
@Observable
class ContentViewModel {
var items: [Item] = []
var isLoading = false
var errorMessage: String?
// Use @ObservationIgnored for non-observed properties
@ObservationIgnored
var internalCache: [String: Any] = [:]
}
```
**Migration steps**:
1. Replace `: ObservableObject` with `@Observable` macro
2. Remove all `@Published` property wrappers
3. Add `@ObservationIgnored` to properties that shouldn't trigger updates
4. Update consuming views (see patterns below)
**Do NOT apply this pattern to `GroupSession` (SharePlay/GroupActivities)**
`GroupSession` is a framework-owned `final class` conforming to `ObservableObject`. You cannot redeclare it, and code observing it must keep using Combine — the SDK ships **no** AsyncSequence for `state`, `activity`, or `activeParticipants` (`sessions()` is the only one).
More importantly, `@Published` publishes from `willSet`, so inside a sink the property still holds the **old** value. The standard late-joiner catch-up depends on exactly that timing:
```swift
groupSession.$activeParticipants
.sink { activeParticipants in
// groupSession.activeParticipants is still the OLD set here
let newParticipants = activeParticipants.subtracting(groupSession.activeParticipants)
// send current state to joiners only
}
```
Rewriting this against `@Observable` or an AsyncSequence makes `subtracting` return an empty set. There is no crash and no warning — late joiners silently never receive state, and the bug only appears with 3+ participants on a device that joined late. Leave Combine observation of `GroupSession` alone and say why.
### Pattern 2: @StateObject → @State (HIGH)
**Why migrate**: Simpler, consistent with value types, works with @Observable
**Requirement**: iOS 17+ with @Observable model
**Detection**:
```
Grep: @StateObject
```
```swift
// ❌ LEGACY
struct ContentView: View {
@StateObject private var viewModel = ContentViewModel()
var body: some View { ... }
}
// ✅ MODERN (with @Observable model)
struct ContentView: View {
@State private var viewModel = ContentViewModel()
var body: some View { ... }
}
```
**Note**: Only migrate after the model uses `@Observable`. If model still uses `ObservableObject`, keep `@StateObject`.
### Pattern 3: @ObservedObject → Direct Property or @Bindable (HIGH)
**Why migrate**: Simpler code, explicit binding when needed
**Requirement**: iOS 17+ with @Observable model
**Detection**:
```
Grep: @ObservedObject
```
```swift
// ❌ LEGACY
struct ItemView: View {
@ObservedObject var item: ItemModel
var body: some View {
Text(item.name)
}
}
// ✅ MODERN - Direct property (read-only access)
struct ItemView: View {
var item: ItemModel // No wrapper needed!
var body: some View {
Text(item.name)
}
}
// ✅ MODERN - @Bindable (for two-way binding)
struct ItemEditorView: View {
@Bindable var item: ItemModel
var body: some View {
TextField("Name", text: $item.name) // Binding works
}
}
```
**Decision tree**:
- Need binding (`$item.property`)? → Use `@Bindable`
- Just reading properties? → Use plain property (no wrapper)
### Pattern 4: @EnvironmentObject → @Environment (HIGH)
**Why migrate**: Type-safe, works with @Observable
**Requirement**: iOS 17+ with @Observable model
**Detection**:
```
Grep: @EnvironmentObject
Grep: \.environmentObject\(
```
```swift
// ❌ LEGACY - Setting
ContentView()
.environmentObject(settings)
// ❌ LEGACY - Reading
struct SettingsView: View {
@EnvironmentObject var settings: AppSettings
var body: some View { ... }
}
// ✅ MODERN - Setting
ContentView()
.environment(settings)
// ✅ MODERN - Reading
struct SettingsView: View {
@Environment(AppSettings.self) var settings
var body: some View { ... }
}
// ✅ MODERN - With binding
struct SettingsEditorView: View {
@Environment(AppSettings.self) var settings
var body: some View {
@Bindable var settings = settings
Toggle("Dark Mode", isOn: $settings.darkMode)
}
}
```
### Pattern 5: onChange(of:perform:) → onChange(of:initial:_:) (MEDIUM)
**Why migrate**: Deprecated modifier, new API has `initial` parameter
**Requirement**: iOS 17+
**Detection**:
```
Grep: \.onChange\(of:.*perform:
```
```swift
// ❌ DEPRECATED
.onChange(of: searchText) { newValue in
performSearch(newValue)
}
// ✅ MODERN (iOS 17+)
.onChange(of: searchText) { oldValue, newValue in
performSearch(newValue)
}
// ✅ With initial execution
.onChange(of: searchText, initial: true) { oldValue, newValue in
performSearch(newValue)
}
```
### Pattern 6: Completion Handlers → async/await (MEDIUM)
**Why migrate**: Cleaner code, better error handling, structured concurrency
**Requirement**: iOS 15+ (widely adopted in iOS 17+)
**Detection**:
```
Grep: completion:\s*@escaping
Grep: completionHandler:
Grep: DispatchQueue\.main\.async
```
```swift
// ❌ LEGACY
func fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
DispatchQueue.main.async {
if let error = error {
completion(.failure(error))
return
}
// Parse and return
completion(.success(user))
}
}.resume()
}
// ✅ MODERN
func fetchUser(id: String) async throws -> User {
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
```
### Pattern 7: withAnimation Closures → Animation Parameter (LOW)
**Why migrate**: Cleaner API, avoids closure
**Requirement**: iOS 17+
**Detection**:
```
Grep: withAnimation.*\{
```
```swift
// ❌ LEGACY
withAnimation(.spring()) {
isExpanded.toggle()
}
// ✅ MODERN (simple cases)
isExpanded.toggle()
// Apply animation to view:
.animation(.spring(), value: isExpanded)
// Or use new binding animation:
$isExpanded.animation(.spring()).wrappedValue.toggle()
```
### Pattern 8: Swift Language Modernization (LOW)
**Why migrate**: Clearer, more efficient, modern Swift idioms
**Detection**:
```
Grep: Date\(\)
Grep: CGFloat
Grep: replacingOccurrences
Grep: DateFormatter\(\)
Grep: \.filter\(.*\)\.count
Grep: Task\.sleep\(nanoseconds:
```
**Reference**: See `axiom-swift (skills/swift-modern.md)` skill for the full modern API replacement table.
Report matches as LOW priority unless they appear in hot paths (then MEDIUM).
## Audit Process
### Step 1: Find Swift Files
```
Glob: **/*.swift
```
### Step 2: Detect Legacy Patterns
**ObservableObject**:
```
Grep: ObservableObject
Grep: @Published
```
**Property Wrappers**:
```
Grep: @StateObject|@ObservedObject|@EnvironmentObject
```
**Deprecated Modifiers**:
```
Grep: onChange\(of:.*perform:
```
**Completion Handlers**:
```
Grep: completion:\s*@escaping
Grep: completionHandler:
```
### Step 3: Categorize by Priority
**HIGH Priority** (significant benefits):
- ObservableObject → @Observable
- Property wrapper migrations
**MEDIUM Priority** (code quality):
- Deprecated modifiers
- async/await adoption
**LOW Priority** (minor improvements):
- Animation syntax
- Minor API updates
## Output Format
```markdown
# Modernization Analysis Results
## Summary
- **HIGH Priority**: [count] (Significant performance/maintainability gains)
- **MEDIUM Priority**: [count] (Deprecated APIs, code quality)
- **LOW Priority**: [count] (Minor improvements)
## Minimum Deployment Target Impact
- Current patterns support: iOS 14+
- After full modernization: iOS 17+
## HIGH Priority Migrations
### ObservableObject → @Observable
**Files affected**: 5
**Estimated effort**: 2-3 hours
#### Models to Migrate
1. `Models/ContentViewModel.swift:12`
```swift
// Current
class ContentViewModel: ObservableObject {
@Published var items: [Item] = []
@Published var isLoading = false
}
// Migrated
@Observable
class ContentViewModel {
var items: [Item] = []
var isLoading = false
}
```
2. `Models/UserSettings.swift:8`
[Similar migration...]
#### Views to Update After Model Migration
| File | Change |
|------|--------|
| `Views/ContentView.swift:15` | `@StateObject` → `@State` |
| `Views/ItemList.swift:23` | `@ObservedObject` → plain property |
| `Views/SettingsView.swift:8` | `@EnvironmentObject` → `@Environment` |
### @EnvironmentObject → @Environment
- `Views/RootView.swift:45`
```swift
// Current
.environmentObject(settings)
// Migrated
.environment(settings)
```
- `Views/SettingsView.swift:12`
```swift
// Current
@EnvironmentObject var settings: AppSettings
// Migrated
@Environment(AppSettings.self) var settings
```
## MEDIUM Priority Migrations
### Deprecated onChange Modifier
- `Views/SearchView.swift:34`
```swift
// Deprecated
.onChange(of: query) { newValue in
search(newValue)
}
// Modern
.onChange(of: query) { oldValue, newValue in
search(newValue)
}
```
### async/await Opportunities
- `Services/NetworkService.swift` - 3 completion handler methods
- `fetchUser(completion:)` → `fetchUser() async throws`
- `fetchItems(completion:)` → `fetchItems() async throws`
- `uploadData(completion:)` → `uploadData() async throws`
## Migration Order
1. **First**: Migrate models to `@Observable`
- All `ObservableObject` → `@Observable`
- Remove all `@Published`
2. **Second**: Update view property wrappers
- `@StateObject` → `@State` (for owned models)
- `@ObservedObject` → plain or `@Bindable`
- `@EnvironmentObject` → `@Environment`
3. **Third**: Update view modifiers
- `.environmentObject()` → `.environment()`
- Deprecated `onChange` syntax
4. **Fourth**: Adopt async/await (optional, but recommended)
## Breaking Changes Warning
⚠️ **Deployment Target**: Full migration requires iOS 17+
If you need to support iOS 16 or earlier:
- Keep `ObservableObject` for those models
- Use conditional compilation:
```swift
#if os(iOS) && swift(>=5.9)
@Observable
class ViewModel { ... }
#else
class ViewModel: ObservableObject { ... }
#endif
```
## Verification
After migration:
1. Build and fix any compiler errors
2. Test view updates (properties should still trigger UI refresh)
3. Test bindings (TextField, Toggle still work)
4. Test environment injection
```
## When No Migration Needed
```markdown
# Modernization Analysis Results
## Summary
Codebase is already using modern patterns!
## Verified
- ✅ Using `@Observable` macro
- ✅ Using `@State` with Observable models
- ✅ Using `@Environment` for shared state
- ✅ No deprecated modifiers detected
## Optional Improvements
- Consider adopting iOS 18+ features when available
- Review remaining completion handlers for async/await conversion
```
## Decision Flowchart
```
Is model a class with published properties?
├─ YES: Does it conform to ObservableObject?
│ ├─ YES: Is it a type you declare (not a framework class)?
│ │ ├─ NO → Keep as-is; report why (e.g. GroupSession)
│ │ └─ YES: Target iOS 17+?
│ │ ├─ YES → Migrate to @Observable
│ │ └─ NO → Keep ObservableObject
│ └─ NO: Already modern or not observable
└─ NO: Check if it's a struct (usually fine)
Is view using @StateObject?
├─ YES: Is the model @Observable?
│ ├─ YES → Change to @State
│ └─ NO → Keep @StateObject until model migrated
└─ NO: Check other wrappers
Is view using @ObservedObject?
├─ YES: Is the model @Observable?
│ ├─ YES: Need binding?
│ │ ├─ YES → Use @Bindable
│ │ └─ NO → Remove wrapper, use plain property
│ └─ NO → Keep @ObservedObject
└─ NO: Already modern
Is view using @EnvironmentObject?
├─ YES: Is the model @Observable?
│ ├─ YES → Change to @Environment(Type.self)
│ └─ NO → Keep @EnvironmentObject
└─ NO: Already modern
```
## False Positives to Avoid
**Not issues**:
- Third-party SDK types using ObservableObject
- Models that intentionally support iOS 14-16
- Combine publishers (not the same as @Published)
- Already migrated code using @Observable
- Apple protocol families unrelated to Observation — classes conforming to `AppIntent`, `EntityQuery`, `AppEntity`, `WidgetConfiguration`, `TimelineProvider`, or other App Intents / WidgetKit protocols are NOT `ObservableObject` and should not be flagged for `@Observable` migration
- `GroupSession` and code observing it (SharePlay/GroupActivities) — a framework-owned `ObservableObject` you cannot redeclare, whose `@Published` willSet timing the participant-delta pattern depends on. See Pattern 1. Migrating it silently breaks late-joiner state catch-up.
**Check before reporting**:
- Verify file is in your project, not dependencies
- Check deployment target constraints
- Confirm model is actually used in SwiftUI views
- Confirm the class actually conforms to `ObservableObject` — do not flag classes just because they are classes
- Confirm the type is yours to change — framework classes conforming to `ObservableObject` cannot be migrated regardless of deployment target
skills/xcode-debugging.md
# 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):
```bash
# 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:
```bash
# List available schemes
xcodebuild -list
```
### For Stale Builds / "No such module" Errors
```bash
# 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
```bash
# 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 the simulator GUI.
# Xcode 26 ships Simulator.app; Xcode 27 ships DeviceHub.app instead and has no
# Simulator.app at all — name both or this silently does nothing on 27.
killall -9 Simulator DeviceHub
# Verify against the PROCESS, not $?. killall exits 0 when EITHER name matched, so
# with both Xcodes installed a 0 can mean "killed Simulator, DeviceHub still running".
pgrep -l Simulator DeviceHub # must print nothing
```
### For Zombie Processes
```bash
# 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 grep
```
### For Test Failures
```bash
# Isolate failing test
xcodebuild test -scheme YourScheme \
-destination 'platform=iOS Simulator,name=iPhone 16' \
-only-testing:YourTests/SpecificTestClass
```
## Simulator Verification (Optional)
After applying fixes, verify in simulator with visual confirmation.
### Quick Screenshot Verification
```bash
# 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).png
```
### Using Axiom Tools
**Quick screenshot**:
```bash
/axiom:screenshot
```
**Full simulator testing** (with navigation, state setup):
```bash
/axiom:test-simulator
```
### When 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:
```bash
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
xcrun simctl io booted screenshot /tmp/fix-verification.png
```
## Decision 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 instead
```
## Common 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
```bash
# 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.plist
```
- `xcsym crash <file>` — Structured crash symbolication with LLM-friendly JSON output. Use for any `.ips`, MetricKit, or legacy `.crash` text file. See `axiom-tools (skills/xcsym-ref.md)`.
## Device Management (devicectl)
`devicectl` is the modern Core Device CLI (Xcode 15+, replaces legacy `idevice*` tools) for installing, launching, and inspecting devices from the command line. Reach for it when an issue doesn't reproduce in Simulator:
```bash
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>
```
`xcrun devicectl list devices` inventories physical devices *and* simulators together (a `Reality` column distinguishes them). For the full tool map, the verified simulator-capable subcommand matrix, `--json-output` parsing keys, and the devicectl-vs-simctl division of labor, see `axiom-tools (skills/device-control-ref.md)`.
## Device Hub (OS27)
Xcode 27 unifies simulators and physical devices in **Device Hub** — a standalone app that auto-launches when you build and run to a simulator (you don't need to open Xcode). Its canvas, the five-panel inspector, and the full GUI reference live in `axiom-tools (skills/device-control-ref.md)`. The canonical *debugging* use is reproducing a device-only bug on a simulator:
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. `simctl` and `devicectl` remain the scriptable path for CI and headless verification — Device Hub is a GUI over the same operations.
## Crash Log Analysis
```bash
# 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.ips
```
## Common 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.