Back to skills
SkillHub ClubShip Full StackFull Stack

swiftui-debugging

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.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
101
Hot score
94
Updated
March 20, 2026
Overall rating
C2.7
Composite score
2.7
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install rshankras-claude-code-apple-skills-swiftui-debugging

Repository

rshankras/claude-code-apple-skills

Skill path: skills/performance/swiftui-debugging

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.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: rshankras.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install swiftui-debugging into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/rshankras/claude-code-apple-skills before adding swiftui-debugging to shared team environments
  • Use swiftui-debugging for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: swiftui-debugging
description: 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.
allowed-tools: [Read, Glob, Grep]
---

# SwiftUI Performance Debugging

Systematic guide for diagnosing and fixing SwiftUI performance problems: unnecessary view re-evaluations, identity issues, expensive body computations, and lazy loading mistakes.

## When This Skill Activates

Use this skill when the user:
- Reports slow or janky SwiftUI views
- Sees excessive view re-renders or body re-evaluations
- Asks about `Self._printChanges()` or view debugging
- Has scrolling performance issues with lists or grids
- Asks why a view keeps updating when nothing changed
- Mentions `@Observable` or `ObservableObject` performance differences
- Wants to understand SwiftUI view identity or diffing
- Uses `AnyView` and asks about performance implications
- Has a hang or stutter traced to SwiftUI rendering

## Decision Tree

```
What 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 Availability

| 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 |

## Top 5 Mistakes -- Quick Reference

| # | 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 |

## Debugging Tools

### Self._printChanges()

Add to any view body to see what triggered re-evaluation:

```swift
var body: some View {
    let _ = Self._printChanges()
    // ... view content
}
```

Output reads: `ViewName: @self, @identity, _propertyName changed.`
See body-reevaluation.md for full interpretation guide.

### Instruments SwiftUI Template

1. **Xcode > Product > Profile** (Cmd+I)
2. Choose **SwiftUI** template (includes View Body, View Properties, Core Animation Commits)
3. Record, reproduce the slow interaction, stop
4. **View Body** lane shows which views had their body evaluated and how often
5. **View Properties** lane shows which properties changed

### os_signpost for Custom Measurement

```swift
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.

## Review Checklist

### View Identity
- [ ] No unstable `.id()` values (random, Date(), array index on mutable arrays)
- [ ] Conditional branches (`if`/`else`) do not cause unnecessary view destruction
- [ ] `ForEach` uses stable, unique identifiers from the model

### Body Re-evaluation
- [ ] Views observe only the properties they actually use
- [ ] `@Observable` classes preferred over `ObservableObject` (iOS 17+)
- [ ] No unnecessary `@State` changes that trigger body re-evaluation
- [ ] Large views split into smaller subviews to narrow observation scope

### Lazy Loading
- [ ] Large collections use `LazyVStack` / `LazyHStack`, not `VStack` / `HStack`
- [ ] `List` or lazy stack used for 50+ items
- [ ] No `.frame(maxHeight: .infinity)` on children inside lazy containers (defeats laziness)

### Common Pitfalls
- [ ] No `AnyView` type erasure (use `@ViewBuilder` or `Group`)
- [ ] No object allocation in `body` (`DateFormatter`, `NSPredicate`, view models)
- [ ] Expensive computations moved to background with `task { }` or `Task.detached`
- [ ] Images use `AsyncImage` or `.resizable()` with proper sizing, not raw `UIImage` decoding in body

## Reference Files

| File | Content |
|------|---------|
| [view-identity.md](view-identity.md) | Structural vs explicit identity, `.id()` usage, conditional branching |
| [body-reevaluation.md](body-reevaluation.md) | What triggers body, `_printChanges()`, `@Observable` vs `ObservableObject` |
| [lazy-loading.md](lazy-loading.md) | Lazy vs eager containers, `List`, `ForEach`, grid performance |
| [common-pitfalls.md](common-pitfalls.md) | `AnyView`, object creation in body, over-observation, expensive computations |
| [../profiling/SKILL.md](../profiling/SKILL.md) | General Instruments profiling (Time Profiler, Memory, Energy) |


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### view-identity.md

```markdown
# View Identity

How SwiftUI decides whether two views are the **same view** (update in place) or **different views** (destroy and recreate). Getting identity wrong causes lost state, broken animations, and unnecessary work.

## Two Kinds of Identity

### Structural Identity (Implicit)

SwiftUI assigns identity based on a view's **position in the view hierarchy**. Two views at the same position in the same parent are considered the same view across renders.

```swift
// These two Text views have different structural identities
// because they are at different positions in the VStack
VStack {
    Text("First")   // position 0
    Text("Second")  // position 1
}
```

### Explicit Identity

You override structural identity using `.id()` or `ForEach` identifiers. This tells SwiftUI: "this specific value identifies this view."

```swift
ForEach(items, id: \.id) { item in
    ItemRow(item: item)  // identity = item.id
}

DetailView(item: selectedItem)
    .id(selectedItem.id)  // identity = selectedItem.id
```

## How SwiftUI Uses Identity

| Identity comparison | SwiftUI behavior |
|--------------------|-----------------|
| Same identity, same type | **Update** existing view (preserves `@State`) |
| Same identity, different type | **Destroy** old view, **create** new view |
| Different identity | **Destroy** old view, **create** new view |
| No identity change | **Diff** the view's properties and update as needed |

Key consequence: when identity changes, **all `@State` is lost** and the view is recreated from scratch.

## .id() Modifier

### When to Use .id()

Force SwiftUI to treat a view as a completely new instance:

```swift
// Force recreation when switching between items
// Without .id(), SwiftUI would try to update the same view,
// which can show stale state or skip the appear animation
DetailView(item: selectedItem)
    .id(selectedItem.id)
```

### Stable vs Unstable Identifiers

```swift
// ✅ Stable: database ID, UUID, or model-provided unique key
ForEach(items, id: \.databaseID) { item in
    ItemRow(item: item)
}

// ✅ Stable: using Identifiable conformance
ForEach(items) { item in  // uses item.id automatically
    ItemRow(item: item)
}

// ❌ Unstable: array index on a mutable array
// When items are inserted/deleted, indices shift and
// SwiftUI matches the wrong data to the wrong view
ForEach(Array(items.enumerated()), id: \.offset) { index, item in
    ItemRow(item: item)
}

// ❌ Unstable: random value or Date()
// Generates a new identity every render, destroying all state
DetailView(item: item)
    .id(UUID())  // NEVER do this -- recreates view every body call
```

### Performance Impact of Unstable .id()

When `.id()` changes every render:
1. SwiftUI destroys the entire view subtree (including all child views)
2. All `@State` and `@StateObject` are lost
3. `onAppear` fires again
4. Animations treat it as a new view (insertion transition, not update)
5. Any in-flight async `task` is cancelled and restarted

This is the single most expensive identity mistake. A view with `N` children all get destroyed and recreated.

## Conditional View Branching

### The if/else Identity Problem

`if`/`else` branches create **different structural identities**:

```swift
// ❌ Problem: toggling isLoading destroys and recreates ContentView
// because the two branches are different structural positions
if isLoading {
    ProgressView()
} else {
    ContentView()
}
```

When `isLoading` changes from `true` to `false`:
- `ProgressView` is destroyed (it existed at the "if-true" branch)
- `ContentView` is created from scratch (it appears at the "if-else" branch)
- All `@State` inside `ContentView` resets

### When Destruction Is Acceptable

If the two branches show completely different content (like loading vs loaded), this destruction is fine and expected. The problem arises when you use `if`/`else` to toggle **modifiers** on the same view:

```swift
// ❌ Bad: destroys and recreates the same Text view
if isHighlighted {
    Text(item.title)
        .foregroundStyle(.yellow)
        .bold()
} else {
    Text(item.title)
        .foregroundStyle(.primary)
}

// ✅ Good: same structural identity, just different modifier values
Text(item.title)
    .foregroundStyle(isHighlighted ? .yellow : .primary)
    .bold(isHighlighted)
```

### Ternary Expressions vs if/else in View Body

```swift
// ✅ Ternary: single structural identity, SwiftUI diffs the values
RoundedRectangle(cornerRadius: 12)
    .fill(isSelected ? Color.blue : Color.gray)
    .frame(height: isExpanded ? 200 : 80)

// ❌ if/else: two different structural identities
if isSelected {
    RoundedRectangle(cornerRadius: 12).fill(.blue)
} else {
    RoundedRectangle(cornerRadius: 12).fill(.gray)
}
```

### AnyView and Identity

`AnyView` erases type information, which breaks structural identity tracking:

```swift
// ❌ AnyView: SwiftUI cannot track structural identity across renders
func makeView() -> AnyView {
    if condition {
        return AnyView(ViewA())
    } else {
        return AnyView(ViewB())
    }
}

// ✅ @ViewBuilder: preserves structural identity
@ViewBuilder
func makeView() -> some View {
    if condition {
        ViewA()
    } else {
        ViewB()
    }
}
```

See common-pitfalls.md for more on `AnyView` performance costs.

## ForEach Identity

### How ForEach Uses Identifiers

`ForEach` maps each data element to a view using the `id` key path. SwiftUI uses these identifiers to:
- Match existing views to updated data (minimizing recreations)
- Animate insertions, removals, and reorderings
- Preserve `@State` in each row

```swift
// ✅ Identifiable conformance (cleanest)
struct Item: Identifiable {
    let id: UUID
    var title: String
}
ForEach(items) { item in
    ItemRow(item: item)
}

// ✅ Explicit id key path
ForEach(items, id: \.uniqueKey) { item in
    ItemRow(item: item)
}
```

### Duplicate Identifiers

If two items share the same `id`, SwiftUI behavior is **undefined** -- it may show duplicates, skip items, or crash:

```swift
// ❌ Dangerous: if two items have the same title, behavior is undefined
ForEach(items, id: \.title) { item in
    ItemRow(item: item)
}

// ✅ Always use a truly unique identifier
ForEach(items, id: \.id) { item in
    ItemRow(item: item)
}
```

## Debugging Identity Issues

### Symptoms of Identity Problems

| Symptom | Likely cause |
|---------|-------------|
| View `@State` resets unexpectedly | Identity changed, causing view recreation |
| `onAppear` fires repeatedly on the same view | Identity keeps changing (unstable `.id()`) |
| Animations show insertion/removal instead of smooth update | View destroyed and recreated instead of updated |
| Scroll position resets in a list | `ForEach` identifiers changed for existing items |
| Text fields lose focus and typed content | The `TextField` view got a new identity |

### Using _printChanges to Detect Recreation

```swift
var body: some View {
    let _ = Self._printChanges()
    // If you see "@identity changed" in the output,
    // SwiftUI considers this a brand-new view instance
    Text("Content")
}
```

Output `@identity changed` means the view was **destroyed and recreated**, not updated in place. This usually means an unstable `.id()` or a structural identity change from `if`/`else` branching.

### Verifying with onAppear / onDisappear

```swift
DetailView(item: item)
    .id(item.id)
    .onAppear { print("DetailView appeared: \(item.id)") }
    .onDisappear { print("DetailView disappeared") }
```

If you see rapid appear/disappear cycles for the same content, the identity is unstable.

## Fix Patterns Summary

| Problem | Fix |
|---------|-----|
| `.id(UUID())` or `.id(Date())` | Use a stable model identifier |
| `if`/`else` toggling modifiers on same view | Use ternary expressions for modifier values |
| `ForEach` with array index as id | Use `Identifiable` conformance or stable key path |
| State lost on navigation | Add `.id(item.id)` to force correct recreation timing |
| Duplicate `ForEach` identifiers | Ensure model has truly unique `id` property |

```

### body-reevaluation.md

```markdown
# Body Re-evaluation

Understanding what triggers SwiftUI to call a view's `body` property, how to diagnose unnecessary re-evaluations, and how to minimize wasted work.

## What Triggers body Re-evaluation

SwiftUI calls `body` when it detects that a view's **dependencies** have changed. Dependencies include:

| Dependency type | Triggers body when... |
|----------------|----------------------|
| `@State` | The wrapped value changes |
| `@Binding` | The source value changes |
| `@Environment` | The environment value changes |
| `@Observable` object (iOS 17+) | A property **that body actually reads** changes |
| `@ObservedObject` / `@EnvironmentObject` | The `objectWillChange` publisher fires (any property) |
| Parent passes new value | An input property's value is different from last render |

Key insight: `body` being called does **not** necessarily mean the view re-renders on screen. SwiftUI diffs the output of `body` against the previous output and only updates what changed. However, calling `body` itself has a cost -- especially for complex view hierarchies.

## Self._printChanges()

The most important debugging tool for re-evaluation. Add it as the first line in `body`:

```swift
struct ItemDetailView: View {
    let item: Item
    @State private var isEditing = false
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        let _ = Self._printChanges()
        // ... view content
    }
}
```

### Reading the Output

Output format: `ViewName: _dependency1, _dependency2 changed.`

| Output | Meaning |
|--------|---------|
| `ItemDetailView: _item changed.` | The `item` property received a new value from the parent |
| `ItemDetailView: _isEditing changed.` | The `@State` property `isEditing` was modified |
| `ItemDetailView: _colorScheme changed.` | The environment's color scheme changed |
| `ItemDetailView: @self changed.` | The view struct itself was recreated (parent's body ran) |
| `ItemDetailView: @identity changed.` | The view's identity changed (destroyed and recreated) |

### @self changed -- What It Means

`@self changed` means the parent view's `body` was called, which recreated this view's struct. Even if all property values are identical, SwiftUI may still call `body` because the struct was freshly allocated.

This is the most common source of "unnecessary" re-evaluations. Fix it by ensuring the parent does not needlessly recreate child views.

### Reducing @self Changes

```swift
// ❌ Parent rebuilds child every time its own body runs
struct ParentView: View {
    @State private var counter = 0

    var body: some View {
        VStack {
            Text("Count: \(counter)")
            Button("+1") { counter += 1 }
            ExpensiveChildView()  // @self changes every time counter changes
        }
    }
}

// ✅ Extract child so its body only runs when its own deps change
struct ParentView: View {
    @State private var counter = 0

    var body: some View {
        VStack {
            CounterSection(counter: $counter)
            ExpensiveChildView()  // Now in its own struct, isolated from counter
        }
    }
}

struct CounterSection: View {
    @Binding var counter: Int

    var body: some View {
        VStack {
            Text("Count: \(counter)")
            Button("+1") { counter += 1 }
        }
    }
}
```

## @Observable vs ObservableObject

This is the single largest performance improvement available in modern SwiftUI.

### ObservableObject: Whole-Object Observation (iOS 13+)

`ObservableObject` notifies **all** observing views when **any** `@Published` property changes:

```swift
// Every view observing this object re-evaluates when ANY property changes
class UserStore: ObservableObject {
    @Published var name = ""           // Change triggers all observers
    @Published var email = ""          // Change triggers all observers
    @Published var avatarURL: URL?     // Change triggers all observers
    @Published var preferences = Preferences()  // Change triggers all observers
}

struct NameLabel: View {
    @ObservedObject var store: UserStore

    var body: some View {
        let _ = Self._printChanges()
        // This body runs when email, avatarURL, or preferences change too!
        Text(store.name)
    }
}
```

### @Observable: Per-Property Observation (iOS 17+)

`@Observable` tracks which properties each view's `body` actually **reads** and only notifies when those specific properties change:

```swift
@Observable
class UserStore {
    var name = ""
    var email = ""
    var avatarURL: URL?
    var preferences = Preferences()
}

struct NameLabel: View {
    var store: UserStore

    var body: some View {
        let _ = Self._printChanges()
        // Only re-evaluates when store.name changes
        // Changes to email, avatarURL, preferences do NOT trigger this view
        Text(store.name)
    }
}
```

### Migration Benefit

| Scenario | ObservableObject | @Observable |
|----------|-----------------|-------------|
| 10 views observe a model with 5 properties | Changing 1 property re-evaluates all 10 views | Only views reading that specific property re-evaluate |
| List of 100 rows, each observing a shared store | All 100 rows re-evaluate on any change | Only affected rows re-evaluate |
| Form with 20 fields bound to a model | Every keystroke re-evaluates all 20 fields | Only the active field re-evaluates |

### @Observable Gotchas

**Reading a property outside body does not create observation:**

```swift
@Observable class Model {
    var value = 0
}

struct MyView: View {
    var model: Model

    // ❌ This read happens in init, not body -- no observation established
    var label: String { "Value: \(model.value)" }

    var body: some View {
        // ✅ Must read model.value inside body (directly or via computed property called from body)
        Text("Value: \(model.value)")
    }
}
```

**Computed properties that read observed properties do establish observation when called from body:**

```swift
@Observable class Model {
    var firstName = ""
    var lastName = ""

    var fullName: String { "\(firstName) \(lastName)" }
}

struct NameView: View {
    var model: Model

    var body: some View {
        // ✅ This observes both firstName and lastName
        // because fullName reads them, and fullName is called from body
        Text(model.fullName)
    }
}
```

## Minimizing Re-evaluations

### Strategy 1: Extract Subviews

The most effective technique. Each extracted subview only re-evaluates when its own dependencies change:

```swift
// ❌ Monolithic: entire view re-evaluates when any state changes
struct ProfileView: View {
    @State private var isEditingName = false
    @State private var isEditingBio = false
    var user: User

    var body: some View {
        VStack {
            // All of this re-evaluates when isEditingBio changes
            HStack {
                Text(user.name)
                Button("Edit") { isEditingName.toggle() }
            }
            // All of this re-evaluates when isEditingName changes
            HStack {
                Text(user.bio)
                Button("Edit") { isEditingBio.toggle() }
            }
        }
    }
}

// ✅ Split: each section only re-evaluates for its own state
struct ProfileView: View {
    var user: User

    var body: some View {
        VStack {
            NameSection(name: user.name)
            BioSection(bio: user.bio)
        }
    }
}

struct NameSection: View {
    let name: String
    @State private var isEditing = false

    var body: some View {
        HStack {
            Text(name)
            Button("Edit") { isEditing.toggle() }
        }
    }
}

struct BioSection: View {
    let bio: String
    @State private var isEditing = false

    var body: some View {
        HStack {
            Text(bio)
            Button("Edit") { isEditing.toggle() }
        }
    }
}
```

### Strategy 2: Use EquatableView or Equatable Conformance

Tell SwiftUI to skip body if properties are equal:

```swift
struct ExpensiveView: View, Equatable {
    let data: LargeDataSet

    static func == (lhs: Self, rhs: Self) -> Bool {
        lhs.data.id == rhs.data.id && lhs.data.version == rhs.data.version
    }

    var body: some View {
        // Complex rendering that should only happen when data truly changes
        ComplexChart(data: data)
    }
}

// Usage: .equatable() tells SwiftUI to use the Equatable conformance
ExpensiveView(data: chartData)
    .equatable()
```

### Strategy 3: Move State Down

Keep `@State` as close to the view that uses it as possible:

```swift
// ❌ State at the top causes the entire tree to re-evaluate
struct ContentView: View {
    @State private var searchText = ""  // Every keystroke re-evaluates everything

    var body: some View {
        NavigationStack {
            VStack {
                SearchBar(text: $searchText)
                ResultsList(query: searchText)
                // ... many other views that don't need searchText
                SidePanel()
                Footer()
            }
        }
    }
}

// ✅ Encapsulate search state in a dedicated view
struct ContentView: View {
    var body: some View {
        NavigationStack {
            VStack {
                SearchSection()  // Owns its own @State
                SidePanel()      // Not affected by search keystrokes
                Footer()         // Not affected by search keystrokes
            }
        }
    }
}

struct SearchSection: View {
    @State private var searchText = ""

    var body: some View {
        VStack {
            SearchBar(text: $searchText)
            ResultsList(query: searchText)
        }
    }
}
```

## Counting Body Evaluations

### Instruments SwiftUI Template

The **View Body** instrument counts how many times each view type's body is called:

1. **Xcode > Product > Profile** (Cmd+I)
2. Choose **SwiftUI** template
3. Record while reproducing the slow interaction
4. The **View Body** lane shows evaluation counts per view type
5. Sort by count to find views that evaluate most frequently

### Manual Counting with os_signpost

```swift
import os

private let logger = Logger(subsystem: "com.app", category: "ViewBody")

struct MyView: View {
    var body: some View {
        let _ = logger.debug("MyView.body evaluated")
        // ... view content
    }
}
```

Filter Console output by your subsystem to see evaluation frequency.

### What Counts Are Acceptable

| Scenario | Expected body count | Concerning if |
|----------|-------------------|---------------|
| View appears once | 1-2 | > 5 |
| Scroll through 100 rows | ~20-30 (visible + prefetch) | 100+ (not lazy) |
| Typing in a text field | 1 per keystroke for the field | 50+ other views also updating |
| Tapping a button | 1-3 for affected views | 20+ unrelated views updating |

## Environment Changes

Environment values like `colorScheme`, `dynamicTypeSize`, and `locale` affect **every** view in the subtree. When they change, a large portion of the view tree re-evaluates.

This is normal and expected -- but avoid reading environment values in views that do not need them:

```swift
// ❌ Reads colorScheme even though it does not use it
struct ItemRow: View {
    @Environment(\.colorScheme) private var colorScheme
    let item: Item

    var body: some View {
        Text(item.title)  // Does not actually use colorScheme
    }
}

// ✅ Only read environment values you actually use in body
struct ItemRow: View {
    let item: Item

    var body: some View {
        Text(item.title)
    }
}
```

```

### lazy-loading.md

```markdown
# Lazy vs Eager Loading

How SwiftUI loads views in stacks, lists, and grids -- and the performance consequences of choosing the wrong container.

## Eager vs Lazy Containers

### Eager Containers

`VStack`, `HStack`, and `ZStack` create **all** child views immediately, regardless of whether they are visible on screen:

```swift
// ❌ Creates ALL 10,000 rows immediately -- hangs on appear
ScrollView {
    VStack {
        ForEach(items) { item in  // 10,000 items
            ItemRow(item: item)   // All 10,000 created at once
        }
    }
}
```

### Lazy Containers

`LazyVStack`, `LazyHStack`, `LazyVGrid`, and `LazyHGrid` create child views **on demand** as they scroll into the visible area:

```swift
// ✅ Only creates visible rows + a small prefetch buffer
ScrollView {
    LazyVStack {
        ForEach(items) { item in  // 10,000 items
            ItemRow(item: item)   // ~20-30 created at a time
        }
    }
}
```

### List

`List` is **always lazy**. It manages its own scroll view and creates rows on demand:

```swift
// ✅ Always lazy -- handles large data sets efficiently
List(items) { item in
    ItemRow(item: item)
}
```

## When to Use Each Container

| Container | Use when | Creates views |
|-----------|----------|---------------|
| `VStack` / `HStack` | Few items (< 50) or all must render at once | All immediately |
| `LazyVStack` / `LazyHStack` | Many items in a `ScrollView` | On demand |
| `LazyVGrid` / `LazyHGrid` | Grid layout with many items | On demand |
| `List` | Scrollable list with system styling (swipe actions, separators) | On demand |

Rule of thumb: if you have a `ForEach` with more than ~50 items inside a `ScrollView`, use a lazy container.

## LazyVStack vs VStack: Performance Comparison

### The Cost of Eager Loading

For a list of 1,000 items, each with a moderately complex row:

| Metric | `VStack` | `LazyVStack` |
|--------|----------|-------------|
| Views created on appear | 1,000 | ~20-30 |
| Time to first frame | Seconds (hang) | Milliseconds |
| Memory at rest | All 1,000 rows in memory | Only visible rows |
| Scroll start latency | None (already created) | Small (creates as you scroll) |

### Practical Threshold

| Item count | Recommendation |
|-----------|---------------|
| 1-20 | `VStack` is fine |
| 20-50 | Either works; use `LazyVStack` if rows are complex |
| 50+ | Always use `LazyVStack` or `List` |
| 500+ | `List` or `LazyVStack` mandatory; consider pagination |

## LazyVStack Configuration

### Pinned Headers

```swift
ScrollView {
    LazyVStack(spacing: 12, pinnedViews: [.sectionHeaders]) {
        ForEach(sections) { section in
            Section {
                ForEach(section.items) { item in
                    ItemRow(item: item)
                }
            } header: {
                SectionHeader(title: section.title)
            }
        }
    }
}
```

### Alignment and Spacing

```swift
LazyVStack(alignment: .leading, spacing: 8) {
    ForEach(items) { item in
        ItemRow(item: item)
    }
}
```

## Common Pitfalls with Lazy Containers

### Pitfall 1: Child Views That Defeat Laziness

If a child view requests infinite height, the lazy container must measure all children to determine layout, defeating the purpose of laziness:

```swift
// ❌ .frame(maxHeight: .infinity) forces the lazy container to measure all children
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
                .frame(maxHeight: .infinity)  // Defeats laziness!
        }
    }
}

// ✅ Use fixed or intrinsic sizing
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
                .frame(height: 60)  // Fixed height, laziness preserved
        }
    }
}
```

### Pitfall 2: GeometryReader Inside Lazy Containers

`GeometryReader` proposes `.infinity` to its children. Inside a lazy container, this can cause layout issues:

```swift
// ❌ GeometryReader inside LazyVStack: layout problems
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            GeometryReader { proxy in
                ItemRow(item: item, width: proxy.size.width)
            }
        }
    }
}

// ✅ Use GeometryReader outside the lazy container
ScrollView {
    GeometryReader { proxy in
        LazyVStack {
            ForEach(items) { item in
                ItemRow(item: item, width: proxy.size.width)
            }
        }
    }
}

// ✅ Or use containerRelativeFrame (iOS 17+)
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
                .containerRelativeFrame(.horizontal)
        }
    }
}
```

### Pitfall 3: ScrollView With No Lazy Container

A `ScrollView` does **not** make its content lazy. It only provides scrolling:

```swift
// ❌ ScrollView alone does NOT make content lazy
ScrollView {
    ForEach(items) { item in  // All created immediately!
        ItemRow(item: item)
    }
}

// ✅ Must wrap in LazyVStack
ScrollView {
    LazyVStack {
        ForEach(items) { item in
            ItemRow(item: item)
        }
    }
}
```

### Pitfall 4: Nesting ScrollViews

Nesting scroll views in the same direction causes undefined behavior:

```swift
// ❌ Nested vertical scroll views -- inner scroll may not work
ScrollView {
    LazyVStack {
        ForEach(sections) { section in
            ScrollView {  // Nested vertical scroll!
                ForEach(section.items) { item in
                    ItemRow(item: item)
                }
            }
        }
    }
}

// ✅ Use a single scroll view with sections
ScrollView {
    LazyVStack(pinnedViews: [.sectionHeaders]) {
        ForEach(sections) { section in
            Section {
                ForEach(section.items) { item in
                    ItemRow(item: item)
                }
            } header: {
                SectionHeader(title: section.title)
            }
        }
    }
}
```

## Grid Performance

### LazyVGrid

```swift
let columns = [
    GridItem(.adaptive(minimum: 150, maximum: 200))
]

ScrollView {
    LazyVGrid(columns: columns, spacing: 16) {
        ForEach(items) { item in
            ItemCell(item: item)
        }
    }
    .padding()
}
```

### Fixed vs Adaptive vs Flexible Columns

| Column type | Behavior | Performance note |
|------------|----------|-----------------|
| `.fixed(200)` | Exact width | Fastest layout calculation |
| `.flexible(minimum: 100, maximum: 300)` | Fills available space | Moderate layout cost |
| `.adaptive(minimum: 150)` | As many as fit | Most layout calculation |

For large grids (500+ items), prefer `.fixed()` columns to minimize layout computation.

## List Performance

### Built-in Optimizations

`List` provides features that lazy stacks do not:
- Cell reuse (like `UITableView`)
- Built-in swipe actions
- Separator management
- Edit mode with reordering and deletion

### List Optimization Tips

```swift
List(items) { item in
    ItemRow(item: item)
        .listRowSeparator(.hidden)  // Slightly faster if you don't need separators
}
.listStyle(.plain)  // .plain is faster than .insetGrouped for large lists
```

### When to Use List vs LazyVStack

| Feature | `List` | `LazyVStack` in `ScrollView` |
|---------|--------|------------------------------|
| Cell reuse | Yes | No (but still lazy creation) |
| Custom styling | Limited | Full control |
| Swipe actions | Built-in | Manual implementation |
| Section headers | Built-in | Manual with `pinnedViews` |
| Scroll performance | Generally better for 1000+ items | Good for most cases |
| Pull to refresh | `.refreshable()` works | `.refreshable()` works |

## Scroll Position and Prefetching

### ScrollViewReader for Programmatic Scrolling

```swift
ScrollViewReader { proxy in
    ScrollView {
        LazyVStack {
            ForEach(items) { item in
                ItemRow(item: item)
                    .id(item.id)
            }
        }
    }
    .onChange(of: selectedItemID) { _, newID in
        withAnimation {
            proxy.scrollTo(newID, anchor: .center)
        }
    }
}
```

### Data Prefetching for Pagination

Lazy containers do not have built-in prefetch callbacks like `UICollectionViewDataSourcePrefetching`. Implement manually:

```swift
struct PaginatedListView: View {
    @State private var items: [Item] = []
    @State private var isLoadingMore = false

    var body: some View {
        ScrollView {
            LazyVStack {
                ForEach(items) { item in
                    ItemRow(item: item)
                        .onAppear {
                            if item == items.last {
                                loadMoreIfNeeded()
                            }
                        }
                }

                if isLoadingMore {
                    ProgressView()
                }
            }
        }
    }

    private func loadMoreIfNeeded() {
        guard !isLoadingMore else { return }
        isLoadingMore = true
        Task {
            let newItems = await fetchNextPage()
            items.append(contentsOf: newItems)
            isLoadingMore = false
        }
    }
}
```

## Debugging Lazy Loading Issues

### Confirming Laziness

Add a print statement to verify views are created lazily:

```swift
struct ItemRow: View {
    let item: Item

    init(item: Item) {
        self.item = item
        print("ItemRow created: \(item.id)")  // Should only print for visible rows
    }

    var body: some View {
        Text(item.title)
    }
}
```

If you see all 1,000 items printed at once, the container is not lazy.

### Measuring Scroll Frame Rate

Use CADisplayLink or the Core Animation Commits instrument in Instruments to measure frame rate during scrolling. Dropped frames (below 60fps / 120fps on ProMotion) indicate scroll performance issues.

```

### common-pitfalls.md

```markdown
# Common SwiftUI Performance Pitfalls

Specific anti-patterns that cause SwiftUI performance problems, with concrete before/after fixes for each.

## 1. AnyView Type Erasure

### The Problem

`AnyView` wraps a view in a type-erased container. SwiftUI uses **type information** for its diffing algorithm -- when types are erased, SwiftUI cannot efficiently diff and must do more work:

- Cannot use structural identity (all `AnyView` instances have the same type)
- Cannot skip diffing subtrees when the type has not changed
- Prevents compile-time optimization of the view hierarchy

### Before (Slow)

```swift
// ❌ AnyView defeats SwiftUI's diffing optimization
func makeView(for item: Item) -> AnyView {
    switch item.kind {
    case .text:
        return AnyView(TextItemView(item: item))
    case .image:
        return AnyView(ImageItemView(item: item))
    case .video:
        return AnyView(VideoItemView(item: item))
    }
}

// Usage in ForEach compounds the problem
ForEach(items) { item in
    makeView(for: item)  // SwiftUI cannot diff between renders
}
```

### After (Fast)

```swift
// ✅ @ViewBuilder preserves type information for the compiler
@ViewBuilder
func makeView(for item: Item) -> some View {
    switch item.kind {
    case .text:
        TextItemView(item: item)
    case .image:
        ImageItemView(item: item)
    case .video:
        VideoItemView(item: item)
    }
}

// ✅ Or better: dedicated view struct (avoids large switch in @ViewBuilder)
struct ItemView: View {
    let item: Item

    var body: some View {
        switch item.kind {
        case .text:
            TextItemView(item: item)
        case .image:
            ImageItemView(item: item)
        case .video:
            VideoItemView(item: item)
        }
    }
}
```

### When AnyView Is Acceptable

`AnyView` has negligible cost in these cases:
- A single view used once (not in a list or `ForEach`)
- Prototyping / throwaway code
- Bridging to UIKit hosting controllers where type erasure is required

## 2. Object Creation in body

### The Problem

`body` can be called frequently (dozens of times per second during animation). Creating objects each time wastes CPU and memory:

```swift
// ❌ Creates a new DateFormatter every time body runs
// DateFormatter init is ~50x more expensive than a simple alloc
var body: some View {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    Text(formatter.string(from: date))
}
```

### Expensive Objects to Watch For

| Object | Cost of creation | Alternative |
|--------|-----------------|-------------|
| `DateFormatter` | ~5-10 microseconds | `static let` shared instance |
| `NumberFormatter` | ~5-10 microseconds | `static let` shared instance |
| `JSONDecoder` / `JSONEncoder` | Moderate | `static let` shared instance |
| `NSRegularExpression` | Moderate (compiles regex) | `static let` or Swift `Regex` literal |
| `NSPredicate` | Moderate | `static let` or reuse via `@State` |
| `URLSession` | Heavy | Use `URLSession.shared` or inject |
| View models / controllers | Heavy | `@State` or `@StateObject` |
| `AttributedString` with markdown | Moderate (parses markdown) | Cache in model or `@State` |

### After (Fast)

```swift
// ✅ Static shared formatter -- created once, reused forever
struct DateLabel: View {
    let date: Date

    private static let formatter: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        return f
    }()

    var body: some View {
        Text(Self.formatter.string(from: date))
    }
}

// ✅ Or use the modern formatted() API which handles caching internally
struct DateLabel: View {
    let date: Date

    var body: some View {
        Text(date.formatted(.dateTime.month().day().year()))
    }
}
```

## 3. Over-Observation with ObservableObject

### The Problem

With `ObservableObject`, changing **any** `@Published` property notifies **all** observers. If a large view observes a store with many properties, it re-evaluates on every change:

```swift
// ❌ All views observing AppState re-evaluate when ANY property changes
class AppState: ObservableObject {
    @Published var user: User?
    @Published var items: [Item] = []
    @Published var searchText = ""        // Typing here re-evaluates everything
    @Published var selectedTab = 0
    @Published var isOnboarding = false
    @Published var notifications: [Notification] = []
}
```

### Fix Option A: Migrate to @Observable (iOS 17+)

```swift
// ✅ Per-property observation -- views only update when their dependencies change
@Observable
class AppState {
    var user: User?
    var items: [Item] = []
    var searchText = ""          // Only views reading searchText re-evaluate
    var selectedTab = 0
    var notifications: [Notification] = []
}
```

### Fix Option B: Split Into Focused Stores (Pre-iOS 17)

```swift
// ✅ Each store has a narrow scope
class UserStore: ObservableObject {
    @Published var user: User?
}

class SearchStore: ObservableObject {
    @Published var searchText = ""
    @Published var results: [Item] = []
}

class NavigationStore: ObservableObject {
    @Published var selectedTab = 0
}
```

### Fix Option C: Extract Subviews to Narrow Scope

```swift
// ❌ Entire view re-evaluates when searchText changes
struct ContentView: View {
    @ObservedObject var store: AppState

    var body: some View {
        VStack {
            TextField("Search", text: $store.searchText)
            ItemList(items: store.items)
            UserBadge(user: store.user)
        }
    }
}

// ✅ Each subview only connects to what it needs
struct ContentView: View {
    @ObservedObject var store: AppState

    var body: some View {
        VStack {
            SearchBar(store: store)
            ItemList(items: store.items)
            UserBadge(user: store.user)
        }
    }
}

struct SearchBar: View {
    @ObservedObject var store: AppState  // Re-evaluates often, but it's tiny

    var body: some View {
        TextField("Search", text: $store.searchText)
    }
}
```

## 4. Expensive Computation in body

### The Problem

`body` runs on the main thread. Any expensive work directly blocks the UI:

```swift
// ❌ Filtering and sorting 10,000 items every time body runs
var body: some View {
    let filtered = items.filter { $0.matchesQuery(searchText) }
    let sorted = filtered.sorted { $0.date > $1.date }

    List(sorted) { item in
        ItemRow(item: item)
    }
}
```

### After: Cache Computed Results

```swift
// ✅ Compute on change, not on every body call
struct ItemListView: View {
    let items: [Item]
    @State private var searchText = ""
    @State private var filteredItems: [Item] = []

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
            List(filteredItems) { item in
                ItemRow(item: item)
            }
        }
        .onChange(of: searchText) { _, newValue in
            filteredItems = items.filter { $0.matchesQuery(newValue) }
                .sorted { $0.date > $1.date }
        }
        .onAppear {
            filteredItems = items.sorted { $0.date > $1.date }
        }
    }
}
```

### After: Offload to Background for Large Data Sets

```swift
// ✅ Background computation for expensive operations
struct ItemListView: View {
    let items: [Item]
    @State private var searchText = ""
    @State private var filteredItems: [Item] = []

    var body: some View {
        VStack {
            TextField("Search", text: $searchText)
            List(filteredItems) { item in
                ItemRow(item: item)
            }
        }
        .task(id: searchText) {
            // Runs on a background cooperative thread
            // Automatically cancelled if searchText changes again
            let query = searchText
            let allItems = items
            let results = await Task.detached {
                allItems.filter { $0.matchesQuery(query) }
                    .sorted { $0.date > $1.date }
            }.value
            filteredItems = results
        }
    }
}
```

## 5. Large Images Without Proper Sizing

### The Problem

Loading full-resolution images in a scrolling list causes memory spikes and decode latency:

```swift
// ❌ Loads full resolution, decodes on main thread
List(photos) { photo in
    Image(uiImage: UIImage(contentsOfFile: photo.path)!)
        .resizable()
        .frame(height: 200)
}
```

### After (Fast)

```swift
// ✅ AsyncImage handles loading and caching
List(photos) { photo in
    AsyncImage(url: photo.url) { image in
        image.resizable().scaledToFill()
    } placeholder: {
        Color.gray.opacity(0.2)
    }
    .frame(height: 200)
    .clipped()
}

// ✅ For local images: thumbnail generation off main thread
struct ThumbnailView: View {
    let imagePath: String
    @State private var thumbnail: UIImage?

    var body: some View {
        Group {
            if let thumbnail {
                Image(uiImage: thumbnail)
                    .resizable()
                    .scaledToFill()
            } else {
                Color.gray.opacity(0.2)
            }
        }
        .frame(height: 200)
        .clipped()
        .task {
            thumbnail = await UIImage(contentsOfFile: imagePath)?
                .byPreparingThumbnail(ofSize: CGSize(width: 400, height: 400))
        }
    }
}
```

## 6. Unnecessary @State Updates

### The Problem

Setting a `@State` variable to its current value still triggers a re-evaluation:

```swift
// ❌ Triggers re-evaluation even when value hasn't changed
.onReceive(timer) { _ in
    currentTime = Date()  // Always a new value, always triggers body
}

// ❌ Setting state in body (causes infinite loop risk)
var body: some View {
    let _ = { isReady = true }()  // NEVER modify state during body
    Text("Hello")
}
```

### After (Fast)

```swift
// ✅ Guard against redundant updates
.onReceive(timer) { _ in
    let newTime = Date()
    // Only update if the displayed value actually changes
    if Calendar.current.component(.second, from: newTime) !=
       Calendar.current.component(.second, from: currentTime) {
        currentTime = newTime
    }
}

// ✅ Use TimelineView for time-based updates (iOS 15+)
TimelineView(.periodic(from: .now, by: 1.0)) { context in
    Text(context.date.formatted(.dateTime.hour().minute().second()))
}
```

## 7. Deep View Hierarchies

### The Problem

Deeply nested view modifiers create large view trees that are expensive to diff:

```swift
// ❌ Each modifier adds a wrapper view to the tree
Text("Hello")
    .padding()
    .background(Color.blue)
    .cornerRadius(8)
    .padding()
    .background(Color.gray)
    .cornerRadius(12)
    .shadow(radius: 4)
    .padding()
    .background(Color.white)
    .cornerRadius(16)
```

### After: Consolidate Modifiers

```swift
// ✅ Flatten by using composite modifiers and fewer nesting levels
Text("Hello")
    .padding(8)
    .background(
        RoundedRectangle(cornerRadius: 8)
            .fill(Color.blue)
    )
    .padding(8)
    .background(
        RoundedRectangle(cornerRadius: 12)
            .fill(Color.gray)
            .shadow(radius: 4)
    )
    .padding(8)
```

### Custom ViewModifier for Reusable Combinations

```swift
// ✅ Encapsulate common modifier combinations
struct CardStyle: ViewModifier {
    func body(content: Content) -> some View {
        content
            .padding()
            .background(
                RoundedRectangle(cornerRadius: 12)
                    .fill(Color(uiColor: .secondarySystemBackground))
            )
    }
}

extension View {
    func cardStyle() -> some View {
        modifier(CardStyle())
    }
}

// Usage: single modifier instead of three
Text("Hello").cardStyle()
```

## 8. Inefficient Conditional Rendering

### The Problem

Using `opacity(0)` or `hidden()` to hide views still keeps them in the view tree and evaluates their body:

```swift
// ❌ View is invisible but still evaluated and in the layout
ExpensiveView()
    .opacity(showExpensive ? 1 : 0)
```

### After: Conditional Inclusion

```swift
// ✅ View is not created at all when hidden
if showExpensive {
    ExpensiveView()
}

// ✅ Or use Group with optional for cleaner syntax
Group {
    if showExpensive {
        ExpensiveView()
    }
}
```

Note: this changes the view's structural identity (see view-identity.md). Use `opacity` when you need to preserve state; use `if` when you want to avoid the evaluation cost.

## Quick Diagnosis Table

| Symptom | Likely pitfall | Fix |
|---------|---------------|-----|
| Entire screen re-renders on any state change | Over-observation (#3) | Migrate to `@Observable` or split stores |
| List scrolling stutters | Eager loading, images (#5) | Use `LazyVStack`, async image loading |
| View body runs 100+ times/second | Unnecessary state updates (#6) | Guard against redundant state changes |
| Memory climbs while scrolling | Full-resolution images (#5) | Use thumbnails, `AsyncImage` |
| Initial load takes seconds | Eager container (#1 in lazy-loading.md) | Switch to lazy container |
| Type checker hangs during build | Large `@ViewBuilder` switch | Extract cases into separate view structs |
| Animation stutters | Expensive computation in body (#4) | Move to `.task {}` or `onChange` |

```

### ../profiling/SKILL.md

```markdown
---
name: performance-profiling
description: Guide performance profiling with Instruments, diagnose hangs, memory issues, slow launches, and energy drain. Use when reviewing app performance or investigating specific bottlenecks.
allowed-tools: [Read, Glob, Grep, Bash]
---

# Performance Profiling

Systematic guide for profiling Apple platform apps using Instruments, Xcode diagnostics, and MetricKit. Covers CPU, memory, launch time, and energy analysis with actionable fix patterns.

## When This Skill Activates

Use this skill when the user:
- Reports app hangs, stutters, or dropped frames
- Needs to profile CPU usage or find hot code paths
- Has memory leaks, high memory usage, or OOM crashes
- Wants to optimize app launch time
- Needs to reduce battery/energy impact
- Asks about Instruments, Time Profiler, Allocations, or Leaks
- Wants to add `os_signpost` or performance measurement to code
- Is preparing for App Store review and needs performance validation

## Decision Tree

```
What performance problem are you investigating?
│
├─ App hangs / stutters / dropped frames / slow UI
│  └─ Read time-profiler.md
│
├─ High memory / leaks / OOM crashes / growing footprint
│  └─ Read memory-profiling.md
│
├─ Slow app launch / time to first frame
│  └─ Read launch-optimization.md
│
├─ Battery drain / thermal throttling / background energy
│  └─ Read energy-diagnostics.md
│
├─ General "app feels slow" (unknown cause)
│  └─ Start with time-profiler.md, then memory-profiling.md
│
└─ Pre-release performance audit
   └─ Read ALL reference files, use Review Checklist below
```

## Quick Reference

| Problem | Instrument / Tool | Key Metric | Reference |
|---------|-------------------|------------|-----------|
| UI hangs > 250ms | Time Profiler + Hangs | Hang duration, main thread stack | time-profiler.md |
| High CPU usage | Time Profiler | CPU % by function, call tree weight | time-profiler.md |
| Memory leak | Leaks + Memory Graph | Leaked bytes, retain cycle paths | memory-profiling.md |
| Memory growth | Allocations | Live bytes, generation analysis | memory-profiling.md |
| Slow launch | App Launch | Time to first frame (pre-main + post-main) | launch-optimization.md |
| Battery drain | Energy Log | Energy Impact score, CPU/GPU/network | energy-diagnostics.md |
| Thermal issues | Activity Monitor | Thermal state transitions | energy-diagnostics.md |
| Network waste | Network profiler | Redundant fetches, large payloads | energy-diagnostics.md |

## Process

### 1. Identify the Problem Category

Ask the user or inspect their description to classify the issue:
- **Responsiveness**: Hangs, stutters, animation drops
- **Memory**: Leaks, growth, OOM crashes
- **Launch**: Slow cold/warm start
- **Energy**: Battery drain, thermal throttling

### 2. Read the Appropriate Reference File

Each file contains:
- Which Instruments template to use
- Step-by-step profiling workflow
- How to interpret results
- Common fix patterns with code examples

### 3. Profile on Real Hardware

Always remind users:
- **Profile on device**, not Simulator (Simulator uses host CPU/memory)
- Use **Release** build configuration (optimizations change behavior)
- Profile with **representative data** (empty databases hide real perf)
- Close other apps to reduce noise

### 4. Apply Fixes and Verify

After identifying bottlenecks:
- Apply targeted fix from the reference file
- Re-profile to confirm improvement
- Add `os_signpost` markers for ongoing monitoring

## Xcode Diagnostic Settings

Recommend enabling these in **Scheme > Run > Diagnostics**:

| Setting | What It Catches |
|---------|-----------------|
| Main Thread Checker | UI work off main thread |
| Thread Sanitizer | Data races |
| Address Sanitizer | Buffer overflows, use-after-free |
| Malloc Stack Logging | Memory allocation call stacks |
| Zombie Objects | Messages to deallocated objects |

## MetricKit Integration

For production monitoring, recommend MetricKit:

```swift
import MetricKit

final class PerformanceReporter: NSObject, MXMetricManagerSubscriber {
    func startCollecting() {
        MXMetricManager.shared.add(self)
    }

    func didReceive(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            // Launch time
            if let launch = payload.applicationLaunchMetrics {
                log("Resume time: \(launch.histogrammedResumeTime)")
            }
            // Hang rate
            if let responsiveness = payload.applicationResponsivenessMetrics {
                log("Hang time: \(responsiveness.histogrammedApplicationHangTime)")
            }
            // Memory
            if let memory = payload.memoryMetrics {
                log("Peak memory: \(memory.peakMemoryUsage)")
            }
        }
    }

    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            if let hangs = payload.hangDiagnostics {
                for hang in hangs {
                    log("Hang: \(hang.callStackTree)")
                }
            }
        }
    }
}
```

## Review Checklist

### Responsiveness
- [ ] No synchronous work on main thread > 100ms
- [ ] No file I/O or network calls on main thread
- [ ] Core Data / SwiftData fetches use background contexts for large queries
- [ ] Images decoded off main thread (use `.preparingThumbnail` or async decoding)
- [ ] `@MainActor` only on code that truly needs UI access

### Memory
- [ ] No retain cycles (check delegate patterns, closures with `self`)
- [ ] Large resources freed when not visible (images, caches)
- [ ] Collections don't grow unbounded (capped caches, pagination)
- [ ] `autoreleasepool` used in tight loops creating ObjC objects

### Launch Time
- [ ] No heavy work in `init()` of `@main App` struct
- [ ] Deferred non-essential initialization (analytics, prefetch)
- [ ] Minimal dynamic frameworks (prefer static linking)
- [ ] No synchronous network calls at launch

### Energy
- [ ] Background tasks use `BGProcessingTaskRequest` appropriately
- [ ] Location accuracy matches actual need (not always `.best`)
- [ ] Timers use `tolerance` to allow coalescing
- [ ] Network requests batched where possible

## References

- **time-profiler.md** — CPU profiling, hang detection, signpost API
- **memory-profiling.md** — Allocations, Leaks, memory graph debugger
- **launch-optimization.md** — App launch phases, cold/warm start optimization
- **energy-diagnostics.md** — Battery, thermal state, network efficiency
- [WWDC: Ultimate Application Performance Survival Guide](https://developer.apple.com/videos/play/wwdc2021/10181/)
- [WWDC: Analyze Hangs with Instruments](https://developer.apple.com/videos/play/wwdc2023/10248/)
- [WWDC: Detect and Diagnose Memory Issues](https://developer.apple.com/videos/play/wwdc2021/10180/)

```

swiftui-debugging | SkillHub