How do #Preview and the macros SwiftUI uses work, and what makes a view easy to preview?

8.9.1 · L2 · Interview

Predict first

A view crashes in its preview with “no ObservableObject of type … found”. What is the most likely fix?

// A: add the missing object with .environmentObject in the preview
// B: remove the preview
// C: change the view to a class
  • A
  • B
  • C

Mental model. A preview is a photo studio where you can pose the screen in every situation (empty, full, dark, huge text) without playing through the app to get there.

#Preview is a freestanding macro: #Preview("Empty state") { ListView(items: []) } declares a preview that Xcode compiles and renders on the canvas, with no PreviewProvider type. You can declare several, pass traits (.landscapeLeft, .sizeThatFitsLayout), set a name, and use @Previewable @State var x = false inside it to make local state for bindings. Previews run your real code in a separate process, so they show the actual layout, Dynamic Type, dark mode and localisation (you can pin colour scheme, size category and locale per preview) and they are cheap regression checks: one preview per interesting state (empty, loading, error, long text, many items, right-to-left).

A view is easy to preview when its inputs are explicit. Pass values and bindings (.constant(…)) rather than reaching into singletons, inject models and environment values at the preview, and keep network and persistence behind protocols with sample implementations. Views that need @EnvironmentObject or an observable model in the environment crash in a preview that forgot to supply it, so the crash message is a clue to a hidden dependency. SwiftUI also leans on other macros: @Observable (Observation), @Entry for environment values, @Model and #Predicate for SwiftData, and @Previewable. The compiler expands macros into ordinary Swift you can inspect in Xcode (Expand Macro), which is the first thing to do when a macro-generated error is confusing. Previews need a buildable target, so a broken build blocks them, and they are not a substitute for running on a device.

The view reads an environment object that nothing in the preview supplied. Providing one in the preview (or passing it explicitly) fixes it.

Common traps

  • Previews that crash on a missing environment object. Inject it in the preview; the crash shows a hidden dependency.
  • Previewing only the happy path. Add empty, error, long-text and large-type previews.

They might follow up with

  • How do you preview a view that takes a binding? .constant(value), or @Previewable @State in the preview.
  • How do you see what a macro generated? Use Xcode’s Expand Macro on the attribute.

Related questions

  • 8.3.2 How does the SwiftUI environment work, and when should you use it instead of passing a parameter?
  • 8.3.3 How does the Observation framework differ from ObservableObject, and how does tracking work?
  • 8.3.1 What do @State, @Binding and @Bindable do, and who owns the data?