← All topics/UIKit & SwiftUI internals

Practical interview questions

Scenario-style prompts with sample answer outlines. Focus is on how you would design and reason in real codebases.

Question 7

SwiftUI lists, identity, and expensive body work

Scrolling stutters in a SwiftUI List with many rows. What causes unnecessary view invalidation, and how do you structure rows for performance?

Answer outline

Remember the rule: stable identity, cheap rows, narrow updates.

Stable identity: SwiftUI decides what changed by comparing view identity. If rows use random ids, freshly-created UUID() values, or array indexes that shift when data reorders, SwiftUI treats old rows as new rows. That causes rebuilds, lost row state, and jumpy scrolling.

Cheap rows: body is not a setup method, it can run many times. Don’t decode JSON, resize full images, create formatters, or do expensive filtering inside a row body. Prepare data in the model, cache formatters, load images asynchronously, and downsample images before they hit the row.

Narrow updates: Keep each row focused on the small piece of data it needs. If one parent state change invalidates the whole list, split rows into smaller views or pass simpler values so SwiftUI can redraw less work.

Async lifecycle: Start side effects from .task / .task(id:) or an injected model, not from body. SwiftUI cancels .task when the view disappears and restarts .task(id:) when the id changes, which is usually a better fit than ad hoc onAppear network calls.

Principles

  • Identity tells SwiftUI whether this is the same row.
  • Body should describe UI, not perform heavy work.
  • Invalidation should be as local as you can make it.

Use a real domain id that stays the same across reloads and reorders.

Stable identity
ForEach(items) { item in          // Item.id is stable
    RowView(item: item)
}

Follow-up angles

  • Bad smell: ForEach(items.indices) is fragile if the list can insert, delete, or reorder.
  • Images: use AsyncImage or an image pipeline with downsampling; avoid full-size decode in row views.
  • Lifecycle: prefer .task(id:) when loading depends on row identity; it gives cancellation semantics that plain onAppear does not encode as clearly.
  • Profiling: use Instruments SwiftUI / Time Profiler when guessing stops helping. Look for repeated body work and broad invalidations.