| 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:
class ViewModel: ObservableObject {
@Published var items: [Item] = []
var count: Int = 0
}
List(items) { item in
ItemRow(item: item)
}
.id(UUID())
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:
struct ParentView: View {
@State private var isOn = false
var body: some View {
ChildView(isOn: $isOn)
}
}
struct ChildView: View {
@Binding var isOn: Bool
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:
NavigationStack {
List(items) { item in
NavigationLink(value: item) {
Text(item.name)
}
}
.navigationDestination(for: Item.self) { item in
DetailView(item: item)
}
}
@State private var path = NavigationPath()
NavigationStack(path: $path) {
}
path.append(item)
4. Memory Leaks with Closures
Symptoms:
- Memory usage grows over time
- Deinit never called
- Retain cycles in view models
Solutions:
viewModel.fetchData { [weak self] result in
guard let self = self else { return }
self.handleResult(result)
}
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:
#Preview {
ContentView()
.environmentObject(MockViewModel())
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.previewDevice("iPhone 15 Pro")
}
}
#endif
#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:
publisher
.receive(on: DispatchQueue.main)
.sink { value in
self.updateUI(value)
}
.store(in: &cancellables)
publisher
.print("DEBUG")
.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:
var body: some View {
VStack {
if condition1 && condition2 || condition3 {
}
}
}
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:
Button("Toggle") {
withAnimation(.spring()) {
isExpanded.toggle()
}
}
Rectangle()
.frame(width: isExpanded ? 200 : 100)
.animation(.easeInOut, value: isExpanded)
var transaction = Transaction(animation: .easeInOut)
transaction.disablesAnimations = false
withTransaction(transaction) {
isExpanded.toggle()
}
Debugging Tools
Xcode Debugger
Breakpoints:
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
var body: some View {
let _ = Self._printChanges()
Text("Hello")
}
#if DEBUG
func debugPrint(_ items: Any...) {
print(items)
}
#else
func debugPrint(_ items: Any...) {}
#endif
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
- Run app in simulator/device
- Click "Debug View Hierarchy" button in Xcode
- Use 3D view to inspect layer structure
- Check for overlapping views, incorrect frames
Environment Inspection
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
-
Create minimal reproduction
- Strip away unrelated code
- Use fresh SwiftUI project if needed
- Test in Preview vs Simulator vs Device
-
Identify trigger conditions
- When does the bug occur?
- What user actions trigger it?
- Is it state-dependent?
-
Check iOS version specifics
- Does it happen on all iOS versions?
- Is it simulator-only or device-only?
Phase 2: Diagnose
-
Use Self._printChanges()
var body: some View {
let _ = Self._printChanges()
}
-
Add strategic breakpoints
- Body property
- State mutations
- Network callbacks
-
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
-
Verify threading
assert(Thread.isMainThread, "Must be on main thread")
Phase 3: Fix
-
Apply targeted fix
- Fix one issue at a time
- Don't introduce new property wrappers unnecessarily
-
Test the fix
- Verify in Preview
- Test in Simulator
- Test on physical device
- Test edge cases
-
Check for side effects
- Run existing tests
- Verify related features still work
Phase 4: Prevent
-
Add unit tests
func testViewModelUpdatesState() async {
let viewModel = ViewModel()
await viewModel.fetchData()
XCTAssertEqual(viewModel.items.count, 10)
}
-
Add UI tests
func testNavigationFlow() {
let app = XCUIApplication()
app.launch()
app.buttons["DetailButton"].tap()
XCTAssertTrue(app.staticTexts["DetailView"].exists)
}
-
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
.id(someValue)
.onAppear { print("View appeared") }
.onDisappear { print("View disappeared") }
.task { print("Task started") }
.border(Color.red)
.background(Color.blue.opacity(0.3))
.background(GeometryReader { geo in
Color.clear.onAppear {
print("Size: \(geo.size)")
print("Frame: \(geo.frame(in: .global))")
}
})
.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
deinit {
print("\(Self.self) deinit")
}
Resources