skills/performance/swiftui-debugging/SKILL.md
Diagnose SwiftUI performance issues including unnecessary re-renders, view identity problems, and slow body evaluations. Use when SwiftUI views are slow, janky, or re-rendering too often.
npx skillsauth add rshankras/claude-code-apple-skills swiftui-debuggingInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Systematic guide for diagnosing and fixing SwiftUI performance problems: unnecessary view re-evaluations, identity issues, expensive body computations, and lazy loading mistakes.
Use this skill when the user:
Self._printChanges() or view debugging@Observable or ObservableObject performance differencesAnyView and asks about performance implicationsWhat SwiftUI performance problem are you seeing?
|
+- Views re-render when they should not
| +- Read body-reevaluation.md
| +- Self._printChanges() to identify which property changed
| +- @Observable vs ObservableObject observation differences
| +- Splitting views to narrow observation scope
|
+- Scrolling is slow / choppy (lists, grids)
| +- Read lazy-loading.md
| +- VStack vs LazyVStack, ForEach without lazy container
| +- List prefetching, grid cell reuse
|
+- Views lose state unexpectedly / animate when they should not
| +- Read view-identity.md
| +- Structural vs explicit identity
| +- .id() misuse, conditional view branching
|
+- Known pitfall (AnyView, DateFormatter in body, etc.)
| +- Read common-pitfalls.md
| +- AnyView type erasure, object creation in body
| +- Over-observation, expensive computations
|
+- General "my SwiftUI app is slow" (unknown cause)
| +- Start with body-reevaluation.md, then common-pitfalls.md
| +- Use Instruments SwiftUI template (see Debugging Tools below)
| API / Technique | Minimum Version | Reference |
|----------------|-----------------|-----------|
| Self._printChanges() | iOS 15 | body-reevaluation.md |
| @Observable | iOS 17 / macOS 14 | body-reevaluation.md |
| @ObservableObject | iOS 13 | body-reevaluation.md |
| LazyVStack / LazyHStack | iOS 14 | lazy-loading.md |
| LazyVGrid / LazyHGrid | iOS 14 | lazy-loading.md |
| .id() modifier | iOS 13 | view-identity.md |
| Instruments SwiftUI template | Xcode 14+ | SKILL.md |
| os_signpost | iOS 12 | SKILL.md |
| # | Mistake | Fix | Details |
|---|---------|-----|---------|
| 1 | Large ForEach inside VStack or ScrollView without lazy container | Wrap in LazyVStack -- eager VStack creates all views upfront | lazy-loading.md |
| 2 | Using AnyView to erase types | Use @ViewBuilder, Group, or concrete generic types -- AnyView defeats diffing | common-pitfalls.md |
| 3 | Creating objects in body (DateFormatter(), NumberFormatter()) | Use static let shared instances or @State for mutable objects | common-pitfalls.md |
| 4 | Observing entire model when only one property is needed | Split into smaller @Observable objects or extract subviews | body-reevaluation.md |
| 5 | Unstable .id() values causing full view recreation every render | Use stable identifiers (database IDs, UUIDs), never array indices or random values | view-identity.md |
Add to any view body to see what triggered re-evaluation:
var body: some View {
let _ = Self._printChanges()
// ... view content
}
Output reads: ViewName: @self, @identity, _propertyName changed.
See body-reevaluation.md for full interpretation guide.
import os
private let perfLog = OSLog(subsystem: "com.app.perf", category: "SwiftUI")
var body: some View {
let _ = os_signpost(.event, log: perfLog, name: "MyView.body")
// ... view content
}
View in Instruments with the os_signpost instrument to count body evaluations per second.
.id() values (random, Date(), array index on mutable arrays)if/else) do not cause unnecessary view destructionForEach uses stable, unique identifiers from the model@Observable classes preferred over ObservableObject (iOS 17+)@State changes that trigger body re-evaluationLazyVStack / LazyHStack, not VStack / HStackList or lazy stack used for 50+ items.frame(maxHeight: .infinity) on children inside lazy containers (defeats laziness)AnyView type erasure (use @ViewBuilder or Group)body (DateFormatter, NSPredicate, view models)task { } or Task.detachedAsyncImage or .resizable() with proper sizing, not raw UIImage decoding in body| File | Content |
|------|---------|
| view-identity.md | Structural vs explicit identity, .id() usage, conditional branching |
| body-reevaluation.md | What triggers body, _printChanges(), @Observable vs ObservableObject |
| lazy-loading.md | Lazy vs eager containers, List, ForEach, grid performance |
| common-pitfalls.md | AnyView, object creation in body, over-observation, expensive computations |
| ../profiling/SKILL.md | General Instruments profiling (Time Profiler, Memory, Energy) |
development
Build, install, and launch an iOS app on a physical iPhone or iPad entirely from the command line (no Xcode GUI), using xcodebuild + devicectl. Use when the user wants to run, test, or screenshot their app on a real device without opening Xcode.
development
Comprehensive iOS development guidance including Swift best practices, SwiftUI patterns, UI/UX review against HIG, and app planning. Use for iOS code review, best practices, accessibility audits, or planning new iOS apps.
development
Build, install, launch, and screenshot an iOS app in the Simulator to verify a change visually. Use when the user wants to run the app, see a change live, screenshot the running app, or confirm a UI fix actually works (not just that it compiles).
development
Audits skills in this repo for consistency, API drift, and structural gaps. Produces a prioritized report grouped by severity (Critical/High/Medium/Low). Use when asked to "audit skills", "check the skill repo for drift", or when planning bulk skill cleanup. Read-only — does not apply fixes.