debug:swiftui

debug:swiftui

Debug SwiftUI application issues systematically. This skill helps diagnose and resolve SwiftUI-specific problems including view update failures, state management issues with @State/@Binding/@ObservedObject, NavigationStack problems, memory leaks from retain cycles, preview crashes, Combine publisher issues, and animation glitches. Provides Xcode debugger techniques, Instruments profiling, and LLDB commands for iOS/macOS development.

0星標
0分支
更新於 1/13/2026
SKILL.md
readonlyread-only
name
debug:swiftui
description

Debug SwiftUI application issues systematically. This skill helps diagnose and resolve SwiftUI-specific problems including view update failures, state management issues with @State/@Binding/@ObservedObject, NavigationStack problems, memory leaks from retain cycles, preview crashes, Combine publisher issues, and animation glitches. Provides Xcode debugger techniques, Instruments profiling, and LLDB commands for iOS/macOS development.

SwiftUI Debugging Guide

A comprehensive guide for systematically debugging SwiftUI applications, covering common error patterns, debugging tools, and step-by-step resolution strategies.

Common Error Patterns

1. View Not Updating

Symptoms:

  • UI doesn't reflect state changes
  • Data updates but view remains stale
  • Animations don't trigger

Root Causes:

  • Missing @Published on ObservableObject properties
  • Using wrong property wrapper (@State vs @Binding vs @ObservedObject)
  • Mutating state on background thread
  • Object reference not triggering SwiftUI's change detection

Solutions:

// Ensure @Published is used for observable properties
class ViewModel: ObservableObject {
    @Published var items: [Item] = []  // Correct
    var count: Int = 0  // Won't trigger updates
}

// Force view refresh with id modifier
List(items) { item in
    ItemRow(item: item)
}
.id(UUID())  // Forces complete rebuild

// Update state on main thread
DispatchQueue.main.async {
    self.viewModel.items = newItems
}

2. @State/@Binding Issues

Symptoms:

  • Child view changes don't propagate to parent
  • State resets unexpectedly
  • Two-way binding doesn't work

Solutions:

// Parent view
struct ParentView: View {
    @State private var isOn = false

    var body: some View {
        ChildView(isOn: $isOn)  // Pass binding with $
    }
}

// Child view
struct ChildView: View {
    @Binding var isOn: Bool  // Use @Binding, not @State

    var body: some View {
        Toggle("Toggle", isOn: $isOn)
    }
}

3. NavigationStack Problems

Symptoms:

  • Navigation doesn't work
  • Back button missing
  • Destination view not appearing
  • Deprecated NavigationView warnings

Solutions:

// iOS 16+ use NavigationStack
NavigationStack {
    List(items) { item in
        NavigationLink(value: item) {
            Text(item.name)
        }
    }
    .navigationDestination(for: Item.self) { item in
        DetailView(item: item)
    }
}

// For programmatic navigation
@State private var path = NavigationPath()

NavigationStack(path: $path) {
    // ...
}

// Navigate programmatically
path.append(item)

4. Memory Leaks with Closures

Symptoms:

  • Memory usage grows over time
  • Deinit never called
  • Retain cycles in view models

Solutions:

// Use [weak self] in closures
viewModel.fetchData { [weak self] result in
    guard let self = self else { return }
    self.handleResult(result)
}

// For Combine subscriptions, store cancellables
private var cancellables = Set<AnyCancellable>()

publisher
    .sink { [weak self] value in
        self?.handleValue(value)
    }
    .store(in: &cancellables)

5. Preview Crashes

Symptoms:

  • Canvas shows "Preview crashed"
  • "Cannot preview in this file"
  • Slow or unresponsive previews

Solutions:

// Provide mock data for previews
#Preview {
    ContentView()
        .environmentObject(MockViewModel())
}

// Use @available to exclude preview-incompatible code
#if DEBUG
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
            .previewDevice("iPhone 15 Pro")
    }
}
#endif

// Simplify preview environment
#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}

6. Combine Publisher Issues

Symptoms:

  • Publisher never emits
  • Multiple subscriptions
  • Memory leaks
  • Values emitted on wrong thread

Solutions:

// Ensure receiving on main thread for UI updates
publisher
    .receive(on: DispatchQueue.main)
    .sink { value in
        self.updateUI(value)
    }
    .store(in: &cancellables)

// Debug publisher chain
publisher
    .print("DEBUG")  // Prints all events
    .handleEvents(
        receiveSubscription: { _ in print("Subscribed") },
        receiveOutput: { print("Output: \($0)") },
        receiveCompletion: { print("Completed: \($0)") },
        receiveCancel: { print("Cancelled") }
    )
    .sink { _ in }
    .store(in: &cancellables)

7. Compiler Type-Check Errors

Symptoms:

  • "The compiler is unable to type-check this expression in reasonable time"
  • Generic error messages on wrong line
  • Build times extremely slow

Solutions:

// Break complex views into smaller components
// BAD: Complex inline logic
var body: some View {
    VStack {
        if condition1 && condition2 || condition3 {
            // Lots of nested views...
        }
    }
}

// GOOD: Extract to computed properties or subviews
var body: some View {
    VStack {
        conditionalContent
    }
}

@ViewBuilder
private var conditionalContent: some View {
    if shouldShowContent {
        ContentSubview()
    }
}

8. Animation Issues

Symptoms:

  • Animations not playing
  • Jerky or stuttering animations
  • Wrong elements animating

Solutions:

// Use withAnimation for explicit control
Button("Toggle") {
    withAnimation(.spring()) {
        isExpanded.toggle()
    }
}

// Apply animation to specific value
Rectangle()
    .frame(width: isExpanded ? 200 : 100)
    .animation(.easeInOut, value: isExpanded)

// Use transaction for fine-grained control
var transaction = Transaction(animation: .easeInOut)
transaction.disablesAnimations = false
withTransaction(transaction) {
    isExpanded.toggle()
}

Debugging Tools

Xcode Debugger

Breakpoints:

// Conditional breakpoint
// Right-click breakpoint > Edit Breakpoint > Condition: items.count > 10

// Symbolic breakpoint for SwiftUI layout issues
// Debug > Breakpoints > Create Symbolic Breakpoint
// Symbol: UIViewAlertForUnsatisfiableConstraints

LLDB Commands:

# Print view hierarchy
po view.value(forKey: "recursiveDescription")

# Print SwiftUI view
po self

# Examine memory
memory read --size 8 --format x 0x12345678

# Find retain cycles
leaks --outputGraph=/tmp/leaks.memgraph [PID]

Instruments

Allocations:

  • Track memory usage over time
  • Identify objects not being deallocated
  • Find retain cycles

Time Profiler:

  • Identify slow code paths
  • Find main thread blocking
  • Optimize view rendering

SwiftUI Instruments (Xcode 15+):

  • View body evaluations
  • View identity tracking
  • State change tracking

Print Debugging

// Track view redraws
var body: some View {
    let _ = Self._printChanges()  // Prints what caused redraw
    Text("Hello")
}

// Conditional debug printing
#if DEBUG
func debugPrint(_ items: Any...) {
    print(items)
}
#else
func debugPrint(_ items: Any...) {}
#endif

// os_log for structured logging
import os.log

let logger = Logger(subsystem: "com.app.name", category: "networking")
logger.debug("Request started: \(url)")
logger.error("Request failed: \(error.localizedDescription)")

View Hierarchy Debugger

  1. Run app in simulator/device
  2. Click "Debug View Hierarchy" button in Xcode
  3. Use 3D view to inspect layer structure
  4. Check for overlapping views, incorrect frames

Environment Inspection

// Print all environment values
struct DebugEnvironmentView: View {
    @Environment(\.self) var environment

    var body: some View {
        let _ = print(environment)
        Text("Debug")
    }
}

The Four Phases (SwiftUI-Specific)

Phase 1: Reproduce and Isolate

  1. Create minimal reproduction

    • Strip away unrelated code
    • Use fresh SwiftUI project if needed
    • Test in Preview vs Simulator vs Device
  2. Identify trigger conditions

    • When does the bug occur?
    • What user actions trigger it?
    • Is it state-dependent?
  3. Check iOS version specifics

    • Does it happen on all iOS versions?
    • Is it simulator-only or device-only?

Phase 2: Diagnose

  1. Use Self._printChanges()

    var body: some View {
        let _ = Self._printChanges()
        // Your view content
    }
    
  2. Add strategic breakpoints

    • Body property
    • State mutations
    • Network callbacks
  3. Check property wrapper usage

    • @State for view-local state
    • @Binding for parent-child communication
    • @StateObject for owned ObservableObject
    • @ObservedObject for passed ObservableObject
    • @EnvironmentObject for dependency injection
  4. Verify threading

    // Check if on main thread
    assert(Thread.isMainThread, "Must be on main thread")
    

Phase 3: Fix

  1. Apply targeted fix

    • Fix one issue at a time
    • Don't introduce new property wrappers unnecessarily
  2. Test the fix

    • Verify in Preview
    • Test in Simulator
    • Test on physical device
    • Test edge cases
  3. Check for side effects

    • Run existing tests
    • Verify related features still work

Phase 4: Prevent

  1. Add unit tests

    func testViewModelUpdatesState() async {
        let viewModel = ViewModel()
        await viewModel.fetchData()
        XCTAssertEqual(viewModel.items.count, 10)
    }
    
  2. Add UI tests

    func testNavigationFlow() {
        let app = XCUIApplication()
        app.launch()
        app.buttons["DetailButton"].tap()
        XCTAssertTrue(app.staticTexts["DetailView"].exists)
    }
    
  3. Document the fix

    • Add code comments explaining why
    • Update team documentation

Quick Reference Commands

Xcode Shortcuts

Shortcut Action
Cmd + R Run
Cmd + B Build
Cmd + U Run tests
Cmd + Shift + K Clean build folder
Cmd + Option + P Resume preview
Cmd + 7 Show debug navigator
Cmd + 8 Show breakpoint navigator

Common Debug Snippets

// Force view identity reset
.id(someValue)

// Track view lifecycle
.onAppear { print("View appeared") }
.onDisappear { print("View disappeared") }
.task { print("Task started") }

// Debug layout
.border(Color.red)  // See frame boundaries
.background(Color.blue.opacity(0.3))

// Debug geometry
.background(GeometryReader { geo in
    Color.clear.onAppear {
        print("Size: \(geo.size)")
        print("Frame: \(geo.frame(in: .global))")
    }
})

// Debug state changes
.onChange(of: someState) { oldValue, newValue in
    print("State changed from \(oldValue) to \(newValue)")
}

Build Settings for Debugging

// In scheme > Run > Arguments > Environment Variables
OS_ACTIVITY_MODE = disable  // Reduce console noise
DYLD_PRINT_STATISTICS = 1   // Print launch time stats

Memory Debugging

// Add to class to track deallocation
deinit {
    print("\(Self.self) deinit")
}

// Enable Zombie Objects
// Edit Scheme > Run > Diagnostics > Zombie Objects

// Enable Address Sanitizer
// Edit Scheme > Run > Diagnostics > Address Sanitizer

Resources

You Might Also Like

Related Skills

fix

fix

243Kdev-testing

Use when you have lint errors, formatting issues, or before committing code to ensure it passes CI.

facebook avatarfacebook
獲取
peekaboo

peekaboo

179Kdev-testing

Capture and automate macOS UI with the Peekaboo CLI.

openclaw avataropenclaw
獲取
frontend-testing

frontend-testing

128Kdev-testing

Generate Vitest + React Testing Library tests for Dify frontend components, hooks, and utilities. Triggers on testing, spec files, coverage, Vitest, RTL, unit tests, integration tests, or write/review test requests.

langgenius avatarlanggenius
獲取
frontend-code-review

frontend-code-review

127Kdev-testing

Trigger when the user requests a review of frontend files (e.g., `.tsx`, `.ts`, `.js`). Support both pending-change reviews and focused file reviews while applying the checklist rules.

langgenius avatarlanggenius
獲取
code-reviewer

code-reviewer

92Kdev-testing

Use this skill to review code. It supports both local changes (staged or working tree) and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability, and adherence to project standards.

google-gemini avatargoogle-gemini
獲取
session-logs

session-logs

90Kdev-testing

Search and analyze your own session logs (older/parent conversations) using jq.

moltbot avatarmoltbot
獲取