Use when SwiftUI view debugging requires systematic investigation - view updates not working after basic troubleshooting, intermittent UI issues, complex state dependencies, or when Self._printChanges() shows unexpected update patterns - systematic diagnostic workflows with Instruments integration
Inherits all available tools
Additional assets for this skill
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Use this skill when:
swiftui-debugging skill patterns but issue persistsUnder pressure, you'll be tempted to shortcuts that hide problems instead of diagnosing them. NEVER do these:
❌ Guessing with random @State/@Observable changes
❌ Adding .id(UUID()) to force updates
❌ Using ObservableObject when @Observable would work (iOS 17+)
❌ Ignoring intermittent issues ("works sometimes")
❌ Shipping without understanding
Before diving into diagnostic patterns, establish baseline environment:
# 1. Verify Instruments setup
xcodebuild -version # Must be Xcode 26+ for SwiftUI Instrument
# 2. Build in Release mode for profiling
xcodebuild build -scheme YourScheme -configuration Release
# 3. Clear derived data if investigating preview issues
rm -rf ~/Library/Developer/Xcode/DerivedData
Time cost: 5 minutes Why: Wrong Xcode version or Debug mode produces misleading profiling data
SwiftUI view issue after basic troubleshooting?
│
├─ View not updating?
│ ├─ Basic check: Add Self._printChanges() temporarily
│ │ ├─ Shows "@self changed" → View value changed
│ │ │ └─ Pattern D1: Analyze what caused view recreation
│ │ ├─ Shows specific state property → That state triggered update
│ │ │ └─ Verify: Should that state trigger update?
│ │ └─ Nothing logged → Body not being called at all
│ │ └─ Pattern D3: View Identity Investigation
│ └─ Advanced: Use SwiftUI Instrument
│ └─ Pattern D2: SwiftUI Instrument Investigation
│
├─ View updating too often?
│ ├─ Pattern D1: Self._printChanges() Analysis
│ │ └─ Identify unnecessary state dependencies
│ └─ Pattern D2: SwiftUI Instrument → Cause & Effect Graph
│ └─ Trace data flow, find broad dependencies
│
├─ Intermittent issues (works sometimes)?
│ ├─ Pattern D3: View Identity Investigation
│ │ └─ Check: Does identity change unexpectedly?
│ ├─ Pattern D4: Environment Dependency Check
│ │ └─ Check: Environment values changing frequently?
│ └─ Reproduce in preview 30+ times
│ └─ If can't reproduce: Likely timing/race condition
│
└─ Preview crashes (after basic fixes)?
├─ Pattern D5: Preview Diagnostics (Xcode 26)
│ └─ Check diagnostics button, crash logs
└─ If still fails: Pattern D2 (profile preview build)
Time cost: 5 minutes
Symptom: Need to understand exactly why view body runs
When to use:
Technique:
struct MyView: View {
@State private var count = 0
@Environment(AppModel.self) private var model
var body: some View {
let _ = Self._printChanges() // Add temporarily
VStack {
Text("Count: \(count)")
Text("Model value: \(model.value)")
}
}
}
Output interpretation:
# Scenario 1: View parameter changed
MyView: @self changed
→ Parent passed new MyView instance
→ Check parent code - what triggered recreation?
# Scenario 2: State property changed
MyView: count changed
→ Local @State triggered update
→ Expected if you modified count
# Scenario 3: Environment property changed
MyView: @self changed # Environment is part of @self
→ Environment value changed (color scheme, locale, custom value)
→ Pattern D4: Check environment dependencies
# Scenario 4: Nothing logged
→ Body not being called
→ Pattern D3: View identity investigation
Common discoveries:
"@self changed" when you don't expect
Property shows changed but you didn't change it
Multiple properties changing together
Verification:
Self._printChanges() call before committingCross-reference: For complex cases, use Pattern D2 (SwiftUI Instrument)
Time cost: 25 minutes
Symptom: Complex update patterns that Self._printChanges() can't fully explain
When to use:
Prerequisites:
Steps:
# Build Release
xcodebuild build -scheme YourScheme -configuration Release
# Launch Instruments
# Press Command-I in Xcode
# Choose "SwiftUI" template
Fix: Move expensive operation to model layer, cache result
Graph nodes:
[Blue node] = Your code (gesture, state change, view body)
[System node] = SwiftUI/system work
[Arrow labeled "update"] = Caused this update
[Arrow labeled "creation"] = Caused view to appear
Common patterns:
# Pattern A: Single view updates (GOOD)
[Gesture] → [State Change in ViewModelA] → [ViewA body]
# Pattern B: All views update (BAD - broad dependency)
[Gesture] → [Array change] → [All list item views update]
└─ Fix: Use granular view models, one per item
# Pattern C: Cascade through environment (CHECK)
[State Change] → [Environment write] → [Many view bodies check]
└─ If environment value changes frequently → Pattern D4 fix
Click on nodes:
Verification:
Cross-reference: swiftui-performance skill for detailed Instruments workflows
Time cost: 15 minutes
Symptom: @State values reset unexpectedly, or views don't animate
When to use:
Root cause: View identity changed unexpectedly
Investigation steps:
// ❌ PROBLEM: Identity changes with condition
if showDetails {
CounterView() // Gets new identity each time showDetails toggles
}
// ✅ FIX: Use .opacity()
CounterView()
.opacity(showDetails ? 1 : 0) // Same identity always
Find: Search codebase for views inside if/else that hold state
// ❌ PROBLEM: .id() changes when data changes
DetailView()
.id(item.id + "-\(isEditing)") // ID changes with isEditing
// ✅ FIX: Stable ID
DetailView()
.id(item.id) // Stable ID
Find: Search codebase for .id( — check if ID values change
// ❌ WRONG: Index-based ID
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
Text(item.name)
}
// ❌ WRONG: Non-unique ID
ForEach(items, id: \.category) { item in // Multiple items per category
Text(item.name)
}
// ✅ RIGHT: Unique, stable ID
ForEach(items, id: \.id) { item in
Text(item.name)
}
Find: Search for ForEach — verify unique, stable IDs
Fix patterns:
| Issue | Fix |
|---|---|
| View in conditional | Use .opacity() instead |
| .id() changes too often | Use stable identifier |
| ForEach jumping | Use unique, stable IDs (UUID or server ID) |
| State resets on navigation | Check NavigationStack path management |
Verification:
Time cost: 10 minutes
Symptom: Many views updating when unrelated data changes
When to use:
Root cause: Frequently-changing value in environment OR too many views reading environment
Investigation steps:
# Search for environment modifiers in current project
grep -r "\.environment(" --include="*.swift" .
Look for:
// ❌ BAD: Frequently changing values
.environment(\.scrollOffset, scrollOffset) // Updates 60+ times/second
.environment(model) // If model updates frequently
// ✅ GOOD: Stable values
.environment(\.colorScheme, .dark)
.environment(appModel) // If appModel changes rarely
Using Pattern D2 (Instruments), check Cause & Effect Graph:
Questions:
Fix A: Remove from environment (if frequently changing):
// ❌ Before: Environment
.environment(\.scrollOffset, scrollOffset)
// ✅ After: Direct parameter
ChildView(scrollOffset: scrollOffset)
Fix B: Use @Observable model (if needed by many views):
// Instead of storing primitive in environment:
@Observable class ScrollViewModel {
var offset: CGFloat = 0
}
// Views depend on specific properties:
@Environment(ScrollViewModel.self) private var viewModel
var body: some View {
Text("\(viewModel.offset)") // Only updates when offset changes
}
Verification:
Time cost: 10 minutes
Symptom: Preview won't load or crashes with unclear error
When to use:
Investigation steps:
Location: Editor menu → Canvas → Diagnostics
What it shows:
# Open crash logs directory
open ~/Library/Logs/DiagnosticReports/
# Look for recent .crash files containing "Preview"
ls -lt ~/Library/Logs/DiagnosticReports/ | grep -i preview | head -5
What to look for:
Create minimal preview:
// Start with empty preview
#Preview {
Text("Test")
}
// If this works, gradually add:
#Preview {
MyView() // Your actual view, but with mock data
.environment(MockModel()) // Provide all dependencies
}
// Find which dependency causes crash
Common issues:
| Error | Cause | Fix |
|---|---|---|
| "Cannot find in scope" | Missing dependency | Add to preview (see example below) |
| "Fatal error: Unexpectedly found nil" | Optional unwrap failed | Provide non-nil value in preview |
| "No such module" | Import missing | Add import statement |
| Silent crash (no error) | State init with invalid value | Use safe defaults |
Fix patterns:
// Missing @Environment
#Preview {
ContentView()
.environment(AppModel()) // Provide dependency
}
// Missing @EnvironmentObject (pre-iOS 17)
#Preview {
ContentView()
.environmentObject(AppModel())
}
// Missing ModelContainer (SwiftData)
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Item.self, configurations: config)
return ContentView()
.modelContainer(container)
}
// State with invalid defaults
@State var selectedIndex = 10 // ❌ Out of bounds
let items = ["a", "b", "c"]
// Fix: Safe default
@State var selectedIndex = 0 // ✅ Valid index
Verification:
Context:
If you hear ANY of these under deadline pressure, STOP and use diagnostic patterns:
❌ "Let me try different property wrappers and see what works"
❌ "It works on my device, must be iOS 26 bug"
❌ "We can roll back if the fix doesn't work"
❌ "Add .id(UUID()) to force refresh"
❌ "Users will accept degraded performance for now"
Total time budget: 90 minutes
# 1. Get exact steps from user report
# 2. Build Release mode
xcodebuild build -scheme YourApp -configuration Release
# 3. Test on device (not simulator)
# 4. Reproduce freeze 3+ times
If can't reproduce: Ask for video recording or device logs from affected users
# Launch Instruments with SwiftUI template
# Command-I in Xcode
# Record while reproducing freeze
# Look for:
# - Long View Body Updates (red bars)
# - Cause & Effect Graph showing update cascade
Find:
Based on diagnostic findings:
If Long View Body Update:
// Example finding: Formatter creation in body
// Fix: Move to cached formatter
If Cascade Update:
// Example finding: All toggle views reading entire settings array
// Fix: Per-toggle view models with granular dependencies
If Environment Issue:
// Example finding: Environment value updating every frame
// Fix: Remove from environment, use direct parameter
# Record new Instruments trace
# Compare before/after:
# - Long updates eliminated?
# - Update count reduced?
# - Freeze gone?
# Test on device 10+ times
Slack to VP + team:
"Diagnostic complete: Settings screen freeze caused by formatter creation
in ToggleRow body (confirmed via SwiftUI Instrument, Long View Body Updates).
Each toggle tap recreated NumberFormatter + DateFormatter for all visible
toggles (20+ formatters per tap).
Fix: Cached formatters in SettingsViewModel, pre-formatted strings.
Verified: Settings screen now responds in <16ms (was 200ms+).
Deploying build 2.1.1 now. Will monitor for next 24 hours."
This shows:
Savings: 22 hours + avoid making it worse
Sometimes managers are right to push for speed. Accept the pressure IF:
✅ You've completed diagnostic protocol (90 minutes) ✅ You know exact view/operation causing issue ✅ You have targeted fix, not a guess ✅ You've verified in Instruments before shipping ✅ You're shipping WITH evidence, not hoping
Document your decision (same as above Slack template)
If pressured to skip diagnostics:
"I understand the urgency. Skipping diagnostics means 80% chance of shipping the wrong fix, committing us to 24 more hours of user suffering. The diagnostic protocol takes 90 minutes total and gives us evidence-based confidence. We'll have the fix deployed in under 2 hours, verified, with no risk of making it worse. The math says diagnostics is the fastest path to resolution."
| Symptom | Likely Cause | First Check | Pattern | Fix Time |
|---|---|---|---|---|
| View doesn't update | Missing observer / Wrong state | Self._printChanges() | D1 | 10 min |
| View updates too often | Broad dependencies | Self._printChanges() → Instruments | D1 → D2 | 30 min |
| State resets | Identity change | .id() modifiers, conditionals | D3 | 15 min |
| Cascade updates | Environment issue | Environment modifiers | D4 | 20 min |
| Preview crashes | Missing deps / Bad init | Diagnostics button | D5 | 10 min |
| Intermittent issues | Identity or timing | Reproduce 30+ times | D3 | 30 min |
| Long updates (performance) | Expensive body operation | Instruments (SwiftUI + Time Profiler) | D2 | 30 min |
Before shipping ANY fix:
| Question | Answer Yes? | Action |
|---|---|---|
| Have you used Self._printChanges()? | No | STOP - Pattern D1 (5 min) |
| Have you run SwiftUI Instrument? | No | STOP - Pattern D2 (25 min) |
| Can you explain in one sentence what caused the issue? | No | STOP - you're guessing |
| Have you verified the fix in Instruments? | No | STOP - test before shipping |
| Did you check for simpler explanations? | No | STOP - review diagnostic patterns |
Answer YES to all five → Ship with confidence
Why it's wrong: You don't know WHY it fixed it
Right approach:
Why it's wrong: Guessing is slower when you're wrong
Right approach:
Why it's wrong: Manual testing ≠ verification
Right approach:
# Launch Instruments with SwiftUI template
# 1. In Xcode: Command-I
# 2. Or from command line:
open -a Instruments
# Build in Release mode (required for accurate profiling)
xcodebuild build -scheme YourScheme -configuration Release
# Clean derived data if needed
rm -rf ~/Library/Developer/Xcode/DerivedData
// Add temporarily to view body
var body: some View {
let _ = Self._printChanges() // Shows update reason
// Your view code
}
Remember: Remove before committing!
# Check preview crash logs
open ~/Library/Logs/DiagnosticReports/
# Filter for recent preview crashes
ls -lt ~/Library/Logs/DiagnosticReports/ | grep -i preview | head -5
# Xcode menu path:
# Editor → Canvas → Diagnostics
# Find environment modifiers
grep -r "\.environment(" --include="*.swift" .
# Find environment object usage
grep -r "@Environment" --include="*.swift" .
# Find view identity modifiers
grep -r "\.id(" --include="*.swift" .
In Instruments (after recording):
Cause & Effect Graph:
swiftui-debugging — Basic troubleshooting for view updates, previews, layoutswiftui-performance — Detailed Instruments workflows, optimization patternsswiftui-layout — Adaptive layout patterns, ViewThatFits, AnyLayoutxcode-debugging — Environment diagnostics, build issuesXcode: 26+ Platforms: iOS 17+, macOS Tahoe+ Framework: SwiftUI + Instruments History: See git log for changes