| name | axiom-swiftui-debugging |
| description | Use when debugging SwiftUI view updates, preview crashes, or layout issues - diagnostic decision trees to identify root causes quickly and avoid misdiagnosis under pressure |
| skill_type | discipline |
| version | 1.3.0 |
| last_updated | Added Self._printChanges() debugging, @Observable patterns (iOS 17+), @Bindable, view identity section, and cross-references to swiftui-performance |
SwiftUI Debugging
Overview
SwiftUI debugging falls into three categories, each with a different diagnostic approach:
- View Not Updating – You changed something but the view didn't redraw. Decision tree to identify whether it's struct mutation, lost binding identity, accidental view recreation, or missing observer pattern.
- Preview Crashes – Your preview won't compile or crashes immediately. Decision tree to distinguish between missing dependencies, state initialization failures, and Xcode cache corruption.
- Layout Issues – Views appearing in wrong positions, wrong sizes, overlapping unexpectedly. Quick reference patterns for common scenarios.
Core principle: Start with observable symptoms, test systematically, eliminate causes one by one. Don't guess.
Requires: Xcode 26+, iOS 17+ (iOS 14-16 patterns still valid, see notes)
Related skills: axiom-xcode-debugging (cache corruption diagnosis), axiom-swift-concurrency (observer patterns), axiom-swiftui-performance (profiling with Instruments), axiom-swiftui-layout (adaptive layout patterns)
Example Prompts
These are real questions developers ask that this skill is designed to answer:
1. "My list item doesn't update when I tap the favorite button, even though the data changed"
→ The skill walks through the decision tree to identify struct mutation vs lost binding vs missing observer
2. "Preview crashes with 'Cannot find AppModel in scope' but it compiles fine"
→ The skill shows how to provide missing dependencies with .environment() or .environmentObject()
3. "My counter resets to 0 every time I toggle a boolean, why?"
→ The skill identifies accidental view recreation from conditionals and shows .opacity() fix
4. "I'm using @Observable but the view still doesn't update when I change the property"
→ The skill explains when to use @State vs plain properties with @Observable objects
5. "Text field loses focus when I start typing, very frustrating"
→ The skill identifies ForEach identity issues and shows how to use stable IDs
When to Use SwiftUI Debugging
Use this skill when
- ✅ A view isn't updating when you expect it to
- ✅ Preview crashes or won't load
- ✅ Layout looks wrong on specific devices
- ✅ You're tempted to bandaid with @ObservedObject everywhere
Use axiom-xcode-debugging instead when
- App crashes at runtime (not preview)
- Build fails completely
- You need environment diagnostics
Use axiom-swift-concurrency instead when
- Questions about async/await or MainActor
- Data race warnings
Debugging Tools
Self._printChanges()
SwiftUI provides a debug-only method to understand why a view's body was called.
Usage in LLDB:
(lldb) expression Self._printChanges()
Temporary in code (remove before shipping):
var body: some View {
let _ = Self._printChanges()
Text("Hello")
}
Output interpretation:
MyView: @self changed
- Means the view value itself changed (parameters passed to view)
MyView: count changed
- Means @State property "count" triggered the update
MyView: (no output)
- Body not being called; view not updating at all
⚠️ Important:
- Prefixed with underscore → May be removed in future releases
- NEVER submit to App Store with _printChanges calls
- Performance impact → Use only during debugging
When to use:
- Need to understand exact trigger for view update
- Investigating unexpected updates
- Verifying dependencies after refactoring
Cross-reference: For complex update patterns, use SwiftUI Instrument → see axiom-swiftui-performance skill
View Not Updating Decision Tree
The most common frustration: you changed @State but the view didn't redraw. The root cause is always one of four things.
Step 1: Can You Reproduce in a Minimal Preview?
#Preview {
YourView()
}
YES → The problem is in your code. Continue to Step 2.
NO → It's likely Xcode state or cache corruption. Skip to Preview Crashes section.
Step 2: Diagnose the Root Cause
Root Cause 1: Struct Mutation
Symptom: You modify a @State value directly, but the view doesn't update.
Why it happens: SwiftUI doesn't see direct mutations on structs. You need to reassign the entire value.
@State var items: [String] = []
func addItem(_ item: String) {
items.append(item)
}
@State var items: [String] = []
func addItem(_ item: String) {
var newItems = items
newItems.append(item)
self.items = newItems
}
@State var items: [String] = []
var itemsBinding: Binding<[String]> {
Binding(
get: { items },
set: { items = $0 }
)
}
Fix it: Always reassign the entire struct value, not pieces of it.
Root Cause 2: Lost Binding Identity
Symptom: You pass a binding to a child view, but changes in the child don't update the parent.
Why it happens: You're passing .constant() or creating a new binding each time, breaking the two-way connection.
@State var isOn = false
ToggleChild(value: .constant(isOn))
@State var name = ""
TextField("Name", text: Binding(
get: { name },
set: { name = $0 }
))
@State var isOn = false
ToggleChild(value: $isOn)
@Observable class Book {
var title = "Sample"
var isAvailable = true
}
struct EditView: View {
@Bindable var book: Book
var body: some View {
TextField("Title", text: $book.title)
Toggle("Available", isOn: $book.isAvailable)
}
}
struct ListView: View {
@State private var books = [Book(), Book()]
var body: some View {
List(books) { book in
@Bindable var book = book
TextField("Title", text: $book.title)
}
}
}
@State var name = ""
@State var nameBinding: Binding<String>?
var body: some View {
if nameBinding == nil {
nameBinding = Binding(
get: { name },
set: { name = $0 }
)
}
return TextField("Name", text: nameBinding!)
}
Fix it: Pass $state directly when possible. For @Observable objects (iOS 17+), use @Bindable. If creating custom bindings (pre-iOS 17), create them in init or cache them, not in body.
Root Cause 3: Accidental View Recreation
Symptom: The view updates, but @State values reset to initial state. You see brief flashes of initial values.
Why it happens: The view got a new identity (removed from a conditional, moved in a container, or the container itself was recreated), causing SwiftUI to treat it as a new view.
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter()
}
Button("Toggle") {
showCounter.toggle()
}
}
}
@State var count = 0
var body: some View {
VStack {
Counter()
.opacity(showCounter ? 1 : 0)
Button("Toggle") {
showCounter.toggle()
}
}
}
@State var count = 0
var body: some View {
VStack {
if showCounter {
Counter()
.id("counter")
}
Button("Toggle") {
showCounter.toggle()
}
}
}
Fix it: Preserve view identity by using .opacity() instead of conditionals, or apply .id() with a stable identifier.
Root Cause 4: Missing Observer Pattern
Symptom: An object changed, but views observing it didn't update.
Why it happens: SwiftUI doesn't know to watch for changes in the object.
class Model {
var count = 0
}
struct ContentView: View {
let model = Model()
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1
}
}
}
@Observable class Model {
var count = 0
}
struct ContentView: View {
@State private var model = Model()
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1
}
}
}
struct ContentView: View {
var model: Model
var body: some View {
Text("\(model.count)")
}
}
@Observable class AppModel {
var count = 0
}
@main
struct MyApp: App {
@State private var model = AppModel()
var body: some Scene {
WindowGroup {
ContentView()
.environment(model)
}
}
}
struct ContentView: View {
@Environment(AppModel.self) private var model
var body: some View {
Text("\(model.count)")
}
}
class Model: ObservableObject {
@Published var count = 0
}
struct ContentView: View {
@StateObject var model = Model()
var body: some View {
Text("\(model.count)")
Button("Increment") {
model.count += 1
}
}
}
struct ContentView: View {
@ObservedObject var model: Model
var body: some View {
Text("\(model.count)")
}
}
Fix it (iOS 17+): Use @Observable macro on your class, then @State to store it. Views automatically track dependencies on properties they read.
Fix it (pre-iOS 17): Use @StateObject if you own the object, @ObservedObject if it's injected, or @EnvironmentObject if it's shared across the tree.
Why @Observable is better (iOS 17+):
- Automatic dependency tracking (only reads trigger updates)
- No
@Published wrapper needed
- Works with
@State instead of @StateObject
- Can pass as plain property instead of
@ObservedObject
See also: Managing model data in your app
Decision Tree Summary
View not updating?
├─ Can reproduce in preview?
│ ├─ YES: Problem is in code
│ │ ├─ Modified struct directly? → Struct Mutation
│ │ ├─ Passed binding to child? → Lost Binding Identity
│ │ ├─ View inside conditional? → Accidental Recreation
│ │ └─ Object changed but view didn't? → Missing Observer
│ └─ NO: Likely cache/Xcode state → See Preview Crashes
Preview Crashes Decision Tree
When your preview won't load or crashes immediately, the three root causes are distinct.
Step 1: What's the Error?
Error Type 1: "Cannot find in scope" or "No such module"
Root cause: Preview missing a required dependency (@EnvironmentObject, @Environment, imported module).
struct ContentView: View {
@EnvironmentObject var model: AppModel
var body: some View {
Text(model.title)
}
}
#Preview {
ContentView()
}
#Preview {
ContentView()
.environmentObject(AppModel())
}
#Preview {
MyCustomView()
}
Fix it: Trace the error, find what's missing, provide it to the preview.
Error Type 2: Fatal error or Silent crash (no error message)
Root cause: State initialization failed at runtime. The view tried to access data that doesn't exist.
struct ListView: View {
@State var selectedIndex = 10
let items = ["a", "b", "c"]
var body: some View {
Text(items[selectedIndex])
}
}
struct DetailView: View {
@State var data: Data?
var body: some View {
Text(data!.title)
}
}
struct ListView: View {
@State var selectedIndex = 0
let items = ["a", "b", "c"]
var body: some View {
if selectedIndex < items.count {
Text(items[selectedIndex])
}
}
}
struct DetailView: View {
@State var data: Data?
var body: some View {
if let data = data {
Text(data.title)
} else {
Text("No data")
}
}
}
Fix it: Review your @State initializers. Check array bounds, optional unwraps, and default values.
Error Type 3: Works fine locally but preview won't load
Root cause: Xcode cache corruption. The preview process has stale information about your code.
Diagnostic checklist:
- Preview worked yesterday, code hasn't changed → Likely cache
- Restarting Xcode fixes it temporarily but returns → Definitely cache
- Same code builds in simulator fine but preview fails → Cache
- Multiple unrelated previews fail at once → Cache
Fix it (in order):
- Restart Preview Canvas:
Cmd+Option+P
- Restart Xcode completely (File → Close Window, then reopen project)
- Nuke derived data:
rm -rf ~/Library/Developer/Xcode/DerivedData
- Rebuild:
Cmd+B
If still broken after all four steps: It's not cache, see Error Types 1 or 2.
Decision Tree Summary
Preview crashes?
├─ Error message visible?
│ ├─ "Cannot find in scope" → Missing Dependency
│ ├─ "Fatal error" or silent crash → State Init Failure
│ └─ No error → Likely Cache Corruption
└─ Try: Restart Preview → Restart Xcode → Nuke DerivedData
Layout Issues Quick Reference
Layout problems are usually visually obvious. Match your symptom to the pattern.
Pattern 1: Views Overlapping in ZStack
Symptom: Views stacked on top of each other, some invisible.
Root cause: Z-order is wrong or you're not controlling visibility.
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red)
}
ZStack {
Rectangle().fill(.blue).zIndex(0)
Rectangle().fill(.red).zIndex(1)
}
ZStack {
Rectangle().fill(.blue)
Rectangle().fill(.red).opacity(0.5)
}
Pattern 2: GeometryReader Sizing Weirdness
Symptom: View is tiny or taking up the entire screen unexpectedly.
Root cause: GeometryReader sizes itself to available space; parent doesn't constrain it.
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
Button("Next") { }
}
VStack {
GeometryReader { geo in
Text("Size: \(geo.size)")
}
.frame(height: 100)
Button("Next") { }
}
Pattern 3: SafeArea Complications
Symptom: Content hidden behind notch, or not using full screen space.
Root cause: .ignoresSafeArea() applied to wrong view.
ZStack {
Color.blue.ignoresSafeArea()
VStack {
Text("Still respects safe area")
}
}
ZStack {
Color.blue
VStack {
Text("Can now use full space")
}
}
.ignoresSafeArea()
ZStack {
Color.blue
VStack { ... }
}
.ignoresSafeArea(edges: .horizontal)
Pattern 4: frame() vs fixedSize() Confusion
Symptom: Text truncated, buttons larger than text, sizing behavior unpredictable.
Root cause: Mixing frame() (constrains) with fixedSize() (expands to content).
Text("Long text here")
.frame(width: 100)
.fixedSize()
Text("Long text here")
.frame(width: 100, alignment: .leading)
.lineLimit(1)
VStack(spacing: 0) {
Text("Small")
.fixedSize()
Text("Large")
.fixedSize()
}
Pattern 5: Modifier Order Matters
Symptom: Padding, corners, or shadows appearing in wrong place.
Root cause: Applying modifiers in wrong order. SwiftUI applies bottom-to-top.
Text("Hello")
.padding()
.cornerRadius(8)
Text("Hello")
.cornerRadius(8)
.padding()
Text("Hello")
.frame(width: 100)
.shadow(radius: 4)
Text("Hello")
.shadow(radius: 4)
.frame(width: 100)
View Identity
Understanding View Identity
SwiftUI uses view identity to track views over time, preserve state, and animate transitions. Understanding identity is critical for debugging state preservation and animation issues.
Two Types of Identity
1. Structural Identity (Implicit)
Position in view hierarchy determines identity:
VStack {
Text("First")
Text("Second")
}
When structural identity changes:
if showDetails {
DetailView()
SummaryView()
} else {
SummaryView()
}
Problem: SummaryView gets recreated each time, losing @State values.
2. Explicit Identity
You control identity with .id() modifier:
DetailView()
.id(item.id)
Common Identity Issues
Issue 1: State Resets Unexpectedly
Symptom: @State values reset to initial values when you don't expect.
Cause: View identity changed (position in hierarchy or .id() value changed).
@State private var count = 0
var body: some View {
VStack {
if showDetails {
CounterView(count: $count)
}
Button("Toggle") {
showDetails.toggle()
}
}
}
var body: some View {
VStack {
CounterView(count: $count)
.opacity(showDetails ? 1 : 0)
Button("Toggle") {
showDetails.toggle()
}
}
}
var body: some View {
VStack {
if showDetails {
CounterView(count: $count)
.id("counter")
}
Button("Toggle") {
showDetails.toggle()
}
}
}
Issue 2: Animations Don't Work
Symptom: View changes but doesn't animate.
Cause: Identity changed, SwiftUI treats as remove + add instead of update.
ForEach(items) { item in
ItemView(item: item)
.id(item.id + "-\(selectedID)")
}
ForEach(items) { item in
ItemView(item: item, isSelected: item.id == selectedID)
.id(item.id)
}
Issue 3: ForEach with Changing Data
Symptom: List items jump around or animate incorrectly.
Cause: Non-unique or changing identifiers.
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
Text(item.name)
}
ForEach(items, id: \.category) { item in
Text(item.name)
}
ForEach(items, id: \.id) { item in
Text(item.name)
}
struct Item: Identifiable {
let id = UUID()
var name: String
}
ForEach(items) { item in
Text(item.name)
}
When to Use .id()
Use .id() to:
- Force view recreation when data changes fundamentally
- Animate transitions between distinct states
- Reset @State when external dependency changes
Example: Force recreation on data change:
DetailView(item: item)
.id(item.id)
Don't use .id() when:
- You just need to update view content (use bindings instead)
- Trying to fix update issues (investigate root cause instead)
- Identity is already stable
Debugging Identity Issues
1. Self._printChanges()
var body: some View {
let _ = Self._printChanges()
}
2. Check .id() modifiers
Search codebase for .id() - are IDs changing unexpectedly?
3. Check conditionals
Views in if/else change position → different identity.
Fix: Use .opacity() or stable .id() instead.
Identity Quick Reference
| Symptom | Likely Cause | Fix |
|---|
| State resets | Identity change | Use .opacity() instead of if |
| No animation | Identity change | Remove .id() or use stable ID |
| ForEach jumps | Non-unique ID | Use unique, stable IDs |
| Unexpected recreation | Conditional position | Add explicit .id() |
See also: WWDC21: Demystify SwiftUI
Pressure Scenarios and Real-World Constraints
When you're under deadline pressure, you'll be tempted to shortcuts that hide problems instead of fixing them.
Scenario 1: "Preview keeps crashing, we ship tomorrow"
Red flags you might hear
- "Just rebuild everything"
- "Delete derived data and don't worry about it"
- "Ship without validating in preview"
- "It works on my machine, good enough"
The danger: You skip diagnosis, cache issue recurs after 2 weeks in production, you're debugging while users hit crashes.
What to do instead (5-minute protocol, total):
- Restart Preview Canvas:
Cmd+Option+P (30 seconds)
- Restart Xcode (2 minutes)
- Nuke derived data:
rm -rf ~/Library/Developer/Xcode/DerivedData (30 seconds)
- Rebuild:
Cmd+B (2 minutes)
- Still broken? Use the dependency or initialization decision trees above
Time cost: 5 minutes diagnosis + 2 minutes fix = 7 minutes total
Cost of skipping: 30 min shipping + 24 hours debug cycle = 24+ hours total
Scenario 2: "View won't update, let me just wrap it in @ObservedObject"
Red flags you might think
- "Adding @ObservedObject everywhere will fix it"
- "Use ObservableObject as a band-aid"
- "Add @Published to random properties"
- "It's probably a binding issue, I'll just create a custom binding"
The danger: You're treating symptoms, not diagnosing. Same view won't update in other contexts. You've just hidden the bug.
What to do instead (2-minute diagnosis):
- Can you reproduce in a minimal preview? If NO → cache corruption (see Scenario 1)
- If YES: Test each root cause in order:
- Does the view have @State that you're modifying directly? → Struct Mutation
- Did the view move into a conditional recently? → View Recreation
- Are you passing bindings to children that have changed? → Lost Binding Identity
- Only if none of above: Missing Observer
- Fix the actual root cause, not with @ObservedObject band-aid
Decision principle: If you can't name the specific root cause, you haven't diagnosed yet. Don't code until you can answer "the problem is struct mutation because...".
Scenario 2b: "Intermittent updates - it works sometimes, not always"
Red flags you might think
- "It must be a threading issue, let me add @MainActor everywhere"
- "Let me try @ObservedObject, @State, and custom Binding until something works"
- "Delete DerivedData and hope cache corruption fixes it"
- "This is unfixable, let me ship without this feature"
The danger: You're exhausted after 2 hours of guessing. You're 17 hours from App Store submission. You're panicking. Every minute feels urgent, so you stop diagnosing and start flailing.
Intermittent bugs are the MOST important to diagnose correctly. One wrong guess now creates a new bug. You ship with a broken view AND a new bug. App Store rejects you. You miss launch.
What to do instead (60-minute systematic diagnosis):
Step 1: Reproduce in preview (15 min)
- Create minimal preview of just the broken view
- Tap/interact 20 times
- Does it fail intermittently, consistently, or never?
- Fails in preview: Real bug in your code, use decision tree above
- Works in preview but fails in app: Cache or environment issue, use Preview Crashes decision tree
- Can't reproduce at all: Intermittent race condition, investigate further
Step 2: Isolate the variable (15 min)
- If it's intermittent in preview: Likely view recreation
- Did the view recently move into a conditional? Remove it and test
- Did you add
if logic that might recreate the parent? Remove it and test
- If it works in preview but fails in app: Likely environment/cache issue
- Try on different device/simulator
- Try after clearing DerivedData
Step 3: Apply the specific fix (30 min)
- Once you've identified view recreation: Use
.opacity() instead of conditionals
- Once you've identified struct mutation: Use full reassignment
- Once you've verified it's cache: Nuke DerivedData properly
Step 4: Verify 100% reliability (until submission)
- Run the same interaction 30+ times
- Test on multiple devices/simulators
- Get QA to verify
- Only ship when it's 100% reproducible (not the bug, the FIX)
Time cost: 60 minutes diagnosis + 30 minutes fix + confidence = submit at 9am
Cost of guessing: 2 hours already + 3 more hours guessing + new bug introduced + crash reports post-launch + emergency patch + reputation damage = miss launch + post-launch chaos
The decision principle: Intermittent bugs require SYSTEMATIC diagnosis. The slower you go in diagnosis, the faster you get to the fix. Guessing is the fastest way to disaster.
Professional script for co-leads who suggest guessing
"I appreciate the suggestion. Adding @ObservedObject everywhere is treating the symptom, not the root cause. The skill says intermittent bugs create NEW bugs when we guess. I need 60 minutes for systematic diagnosis. If I can't find the root cause by then, we'll disable the feature and ship a clean v1.1. The math shows we have time—I can complete diagnosis, fix, AND verification before the deadline."
Scenario 3: "Layout looks wrong on iPad, we're out of time"
Red flags you might think
- "Add some padding and magic numbers"
- "It's probably a safe area thing, let me just ignore it"
- "Let's lock this to iPhone only"
- "GeometryReader will solve this"
The danger: Magic numbers break on other sizes. SafeArea ignoring is often wrong. Locking to iPhone means you ship a broken iPad experience.
What to do instead (3-minute diagnosis):
- Run in simulator or device
- Use Debug View Hierarchy: Debug menu → View Hierarchy (takes 30 seconds to load)
- Check: Is the problem SafeArea, ZStack ordering, or GeometryReader sizing?
- Use the correct pattern from the Quick Reference above
Time cost: 3 minutes diagnosis + 5 minutes fix = 8 minutes total
Cost of magic numbers: Ship wrong, report 2 weeks later, debug 4 hours, patch in update = 2+ weeks delay
Quick Reference
Common View Update Fixes
@State var items: [String] = []
var newItems = items
newItems.append("new")
self.items = newItems
@State var value = ""
ChildView(text: $value)
View().opacity(isVisible ? 1 : 0)
@StateObject var model = MyModel()
@ObservedObject var model: MyModel
Common Preview Fixes
#Preview {
ContentView()
.environmentObject(AppModel())
}
@State var index = 0
Common Layout Fixes
Rectangle().zIndex(1)
GeometryReader { geo in ... }.frame(height: 100)
ZStack { ... }.ignoresSafeArea()
Text().cornerRadius(8).padding()
Real-World Examples
Example 1: List Item Doesn't Update When Tapped
Scenario: You have a list of tasks. When you tap a task to mark it complete, the checkmark should appear, but it doesn't.
Code:
struct TaskListView: View {
@State var tasks: [Task] = [...]
var body: some View {
List {
ForEach(tasks, id: \.id) { task in
HStack {
Image(systemName: task.isComplete ? "checkmark.circle.fill" : "circle")
Text(task.title)
Spacer()
Button("Done") {
task.isComplete.toggle()
}
}
}
}
}
}
Diagnosis using the skill:
- Can you reproduce in preview? YES
- Are you modifying the struct directly? YES → Struct Mutation (Root Cause 1)
Fix:
Button("Done") {
if let index = tasks.firstIndex(where: { $0.id == task.id }) {
tasks[index].isComplete.toggle()
}
}
Why this works: SwiftUI detects the array reassignment, triggering a redraw. The task in the List updates.
Example 2: Preview Crashes with "No Such Module"
Scenario: You created a custom data model. It works fine in the app, but the preview crashes with "Cannot find 'CustomModel' in scope".
Code:
import SwiftUI
#Preview {
TaskDetailView(task: Task(...))
}
struct TaskDetailView: View {
@Environment(\.modelContext) var modelContext
let task: Task
var body: some View {
Text(task.title)
}
}
Diagnosis using the skill:
- What's the error? "Cannot find in scope" → Missing Dependency (Error Type 1)
- What does TaskDetailView need? The Task model and modelContext
Fix:
#Preview {
let config = ModelConfiguration(isStoredInMemoryOnly: true)
let container = try! ModelContainer(for: Task.self, configurations: config)
return TaskDetailView(task: Task(title: "Sample"))
.modelContainer(container)
}
Why this works: Providing the environment object and model container satisfies the view's dependencies. Preview loads successfully.
Example 3: Text Field Value Changes Don't Appear
Scenario: You have a search field. You type characters, but the text doesn't appear in the UI. However, the search results DO update.
Code:
struct SearchView: View {
@State var searchText = ""
var body: some View {
VStack {
TextField("Search", text: .constant(searchText))
Text("Results for: \(searchText)")
List {
ForEach(results(for: searchText), id: \.self) { result in
Text(result)
}
}
}
}
func results(for text: String) -> [String] {
}
}
Diagnosis using the skill:
- Can you reproduce in preview? YES
- Are you passing a binding to a child view? YES (TextField)
- Is it a constant binding? YES → Lost Binding Identity (Root Cause 2)
Fix:
TextField("Search", text: $searchText)
Why this works: $searchText passes a two-way binding. TextField writes changes back to @State, triggering a redraw. Text field now shows typed characters.
Simulator Verification
After fixing SwiftUI issues, verify with visual confirmation in the simulator.
Why Simulator Verification Matters
SwiftUI previews don't always match simulator behavior:
- Different rendering — Some visual effects only work on device/simulator
- Different timing — Animations may behave differently
- Different state — Full app lifecycle vs isolated preview
Use simulator verification for:
- Layout fixes (spacing, alignment, sizing)
- View update fixes (state changes, bindings)
- Animation and gesture issues
- Before/after visual comparison
Quick Verification Workflow
/axiom:screenshot
xcodebuild build -scheme YourScheme
/axiom:screenshot
Navigating to Problem Screens
If the bug is deep in your app, use debug deep links to navigate directly:
xcrun simctl openurl booted "debug://problem-screen"
sleep 1
/axiom:screenshot
Full Simulator Testing
For complex scenarios (state setup, multiple steps, log analysis):
/axiom:test-simulator
Then describe what you want to test:
- "Navigate to the recipe editor and verify the layout fix"
- "Test the profile screen with empty state"
- "Verify the animation doesn't stutter anymore"
Before/After Example
Before fix (view not updating):
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/before-fix.png
After fix (added @State binding):
xcrun simctl openurl booted "debug://recipe-list"
sleep 1
xcrun simctl io booted screenshot /tmp/after-fix.png
Time saved: 60%+ faster iteration with visual verification vs manual navigation
Resources
WWDC: 2025-256, 2025-306, 2023-10160, 2023-10149, 2021-10022
Docs: /swiftui/managing-model-data-in-your-app, /swiftui, /swiftui/state-and-data-flow, /xcode/previews, /observation
Skills: axiom-swiftui-performance, axiom-swiftui-debugging-diag, axiom-xcode-debugging, axiom-swift-concurrency