On this page 10 sections
- Previews as a QA surface, not a toy
- Traits you should always show
- Sample data and injected services
- Light, dark, Dynamic Type and Dynamic Island
- Keeping previews fast and reliable
- From a preview to a snapshot test
- When a preview is the wrong tool
- A short team checklist
- Key takeaways
- Frequently asked questions
This is not a first-time tutorial on writing a preview. It assumes you can already pin a view in the canvas. It complements accessible iOS apps in SwiftUI and SwiftUI state and architecture: previews are how you see those decisions without running the whole app.
Previews as a QA surface, not a toy
A useful preview answers a question a reviewer would ask: what happens when the title wraps, when the list is empty, when the list has 40 items, when the type size is Accessibility XL, when the appearance is dark, when the device is the smallest one you support? If the only preview is the marketing state of a screen, you have a picture, not QA.
That means:
- Named previews for states, not one anonymous preview per file. “Account — loading”, “Account — error”, “Account — long name, extra-large type”.
- The same states the tests use. If the model can be empty, failed, or populated, the preview can, and a snapshot can.
- A short, agreed list of traits the team always checks on a new screen, so “I looked at the preview” has a meaning.
You still run the app. Previews will not show you a payment sheet, a permission prompt, or a navigation bug that only exists in a mixed UIKit stack. They will show you the visual and copy problems that, in practice, are most of a small app’s design QA.
Traits you should always show
Start with a set you can stand behind, and add device-specific previews only when the layout actually changes.
A reasonable always-on set for a new screen:
- Light and dark colour schemes.
- The default Dynamic Type size, plus at least one Accessibility size (X Large or XX Large is a useful first stretch).
- The smallest width you support (a compact iPhone) and, if you ship iPad, a regular-width layout.
- The locale you actually ship first, which for my work is usually
en_GB.
import SwiftUI
#Preview("Account — loaded") {
AccountView(model: .previewLoaded)
}
#Preview("Account — loaded, dark, large type") {
AccountView(model: .previewLoaded)
.preferredColorScheme(.dark)
.environment(\.dynamicTypeSize, .accessibility1)
.environment(\.locale, Locale(identifier: "en_GB"))
}
#Preview("Account — empty") {
AccountView(model: .previewEmpty)
}The #Preview macro also accepts traits for orientation and for a specific device. Use a named device when you are checking something device-specific — a Dynamic Island overlap, a home-indicator collision, a landscape form — not as a default for every file. A stack of ten device frames for a settings row will slow the canvas down and tell you almost nothing.
For layout at different widths, a PreviewLayout or a container with a fixed width is often faster than a full device. If you need to reason about breakpoints, the breakpoint tester is a handy way to think about widths on the web; in SwiftUI, a few explicit widths in previews play a similar role.
Do not forget right-to-left if you ship such a language, and do not forget a longer language if you have one. For UK-only English, still include a wrapping title and a long error message; accessibility sizes will force the same issue.
Sample data and injected services
A preview that talks to the live network, or to a real keychain, is a preview that will fail in a meeting. Inject data at the edge the architecture guide already wants you to have: a model, or a protocol for a service, constructed in memory.
extension AccountModel {
static var previewLoaded: AccountModel {
let model = AccountModel(client: .preview)
model.name = "Lee Johnson"
model.plan = "Pro"
return model
}
static var previewEmpty: AccountModel {
AccountModel(client: .preview)
}
static var previewError: AccountModel {
let model = AccountModel(client: .preview)
model.errorMessage = "Could not load your account. Check your connection and try again."
return model
}
}A preview client returns fixtures immediately, does not sleep unless you are deliberately previewing a skeleton for a second, and never writes to disk. If a view requires an Environment object, set it in the preview; do not reach into a singleton from init so the preview can “just work”.
Keep copy realistic. “Foo” and “asdf” will not wrap the way “North Harbour Books membership invoice” will. Use the same tone you ship, including UK English if that is the product. A loading state, an error with a retry, and a long string are part of the fixture set, not extras.
If the view is behind a feature flag or entitlements, include a preview with the flag off. That is the state most new users will see.
Light, dark, Dynamic Type and Dynamic Island
These four deserve a slightly harder look than a generic trait list.
Light and dark should both be readable, not merely inverted. Check contrast on text, on placeholders, on separators, and on custom tints. If you are choosing a pair of colours, a contrast checker will tell you whether a label meets WCAG contrast; the system will not fix a too-light grey you hard-coded for dark mode. Watch hardcoded Color.black or UIColor.white that made sense in one scheme only. Prefer semantic colours (Color.primary, assets with a dark variant).
Colour-blindness is not a SwiftUI trait you can flip with one modifier, but it is part of design QA. If a status is only a red or green dot, preview a state where the meaning is also in text or a symbol. A colour-blindness simulator on a screenshot will show you whether the chart still works; the preview is where you add the text alternative.
Dynamic Type should reflow, not clip. Use the built-in text styles, avoid fixed heights on labels, and check a screen at an accessibility size in both schemes. Buttons with long UK strings (“Personalise notifications”) are regular failures. The accessibility article covers the view-side work; the preview is how you see it daily.
Dynamic Island and notches only appear on the right devices. Preview on one such device when you have a custom header, a full-bleed image, or an overlay that sits at the top. The island should not cover a close button or a title. Safe area insets exist for this; if you ignored them to hide a gap, the preview will show the cost.
Also open the keyboard once in a running session for forms. Previews are weak at keyboard avoidance, and that is a known limit, not something to fake in every canvas.
Keeping previews fast and reliable
A slow or crashing canvas is a canvas people will stop using. Most of the speed work is about not doing real app start-up in the preview.
- Do not start a live
URLSession, a StoreKit listener, or a location manager from a previewed view’sinitor.taskunless you have injected a stub that returns immediately. A.taskthat hits the network will run in the canvas. - Avoid enormous view trees in one file. A tab view that previews the entire app, five times, is why the canvas is compiling half the target. Preview the screen, not the world.
- Be careful with
#Previewthat instantiates every trait as a separate heavy device. Group colour scheme and type size on one view first. - Keep preview data in-process. Reading a 20-megapixel image from the bundle for a thumbnail preview is a good way to jank the canvas; use a small fixture.
- If a preview crashes, treat it as a test failure. The usual causes are a force-unwrap on missing injected state, a missing environment object, or work that assumed a live app delegate. Fix the injection; do not comment out the preview and move on.
- Pin the preview to a previewable slice when the real view is bound to a huge graph. A
AccountHeaderpreview plus anAccountViewpreview is better than one preview you are afraid to open.
Xcode’s canvas also works better when the project compiles quickly. If previews are consistently stale, the problem may be the build, not the #Preview syntax. That is a CI and module-boundary problem, not a reason to abandon the workflow.
From a preview to a snapshot test
A preview is a human check. A snapshot is a regression net. The useful move is to share the configurations, so a state you preview is a state you can assert.
The exact snapshot machinery is your choice: a snapshot-testing library that renders a SwiftUI view to an image, or a tightly scoped UI test that screenshots a hosted view. What matters is:
- The view is constructed the same way as in the preview, with the same fixtures and traits.
- The snapshot is named after the state (“account-empty-dark-ax1”), not after the date you captured it.
- You review diffs, you do not blindly regenerate them on every machine. Font rendering and OS version can change pixels; pin the OS you record on, and keep the set small.
- You snapshot a handful of states per screen, not every combination of device and type size. Combinatorics will bury you. Pick the combinations that have actually broken.
Swift Testing can wrap the assertions; the snapshot library or the UI test provides the image. I would not try to snapshot from the canvas itself as the system of record — the canvas is for looking, the test is for pinning.
When a designer changes padding, the snapshot should fail, someone should look at the image, and the record should be updated on purpose. That is the same kind of conversation a preview is meant to start, only now it happens in CI.
When a preview is the wrong tool
Do not force previews to be the whole QA story.
They are the wrong tool, or a weak one, for:
- Navigation stacks, deep links and UIKit hosting, which depend on a real path and a real parent. Cover those with unit tests on the path and a few UI tests, as in the NavigationStack and UIKit-migration guides in this series.
- StoreKit, permissions, camera, push and background tasks, which need the system or a specialised test harness.
- Performance. A smooth canvas does not mean a smooth
Listof a thousand real photos. Use Instruments when the question is dropped frames. - Motion, keyboard and pointer. You can approximate some of this; you cannot honestly sign it off only in a preview.
- “Does this make sense as a product?” A preview will not tell you that a settings screen has the wrong items. Someone still has to use the app.
If a view is a thin wrapper around a UIViewRepresentable, preview the representable’s interesting states if you can, but also run it in the app. Preview bugs in representables are common, because updateUIView and layout behave slightly differently in the canvas.
A short team checklist
For each new or substantially changed screen, before you call the visual work done:
- Named previews exist for loading, empty, error and a typical populated state.
- Light and dark are both shown, and contrast on custom colours has been checked.
- At least one accessibility Dynamic Type size is shown, and nothing useful is clipped.
- The first shipped locale is shown, with realistic copy, not “Foo”.
- A compact width is shown; iPad or landscape is shown if you claim to support it.
- Services are injected; the canvas does not need the network or a logged-in account.
- The preview opens quickly and does not crash.
- Snapshot or screenshot coverage has been updated for the states that matter, or you have written down why this screen is look-only.
That list should take minutes once the fixtures exist. If it takes an afternoon, the view is doing too much in its init, or the team does not yet have a preview data library. Build the library; it pays back on the next screen.
Key takeaways
- Use previews to answer QA questions: empty, error, long copy, dark mode, large type, small width.
- Inject in-memory models and stub services. A preview that needs the live app is not a preview you will use.
- Always check light and dark, a stretched Dynamic Type size, and the smallest width; use a specific device when the Dynamic Island or home indicator can overlap your layout.
- Keep the canvas fast: small fixtures, no network, no full-app trees, crash-fixes treated as test failures.
- Reuse the same states in a small snapshot set on CI, reviewed when they fail, not regenerated by habit.
- Still run the app for navigation, system sheets, performance and keyboard.
If you want help turning a screen’s previews into a QA set, tell me about it.
Frequently asked questions
Why is my SwiftUI preview so slow or crashing?
Usually because the previewed view starts real networking, StoreKit, or a large part of the app, or because it force-unwraps an environment object you only set in the running app. Inject stubs, preview a smaller slice, and treat a crashing canvas as a missing dependency, not as a canvas bug. A huge matrix of full device frames will also make the canvas lag.
How do you preview Dynamic Type sizes in SwiftUI?
Set .environment(\.dynamicTypeSize, .accessibility1) (or another size) on the previewed view, and use the built-in text styles in the view itself. Look at the layout at that size in light and dark. If a label clips, the fix is flexible layout, not a smaller font that ignores the user’s setting.
How do you give a SwiftUI preview sample data?
Construct the same model or service the view already uses, in memory, with fixtures for loading, empty, error and populated states. Pass it in the preview, or set it on the environment, rather than reaching into a singleton. Use realistic copy so wrapping and tone are visible.
Can you snapshot-test a SwiftUI preview?
You can snapshot the same view, in the same configuration, that a preview shows, using a snapshot-testing library or a targeted UI test. Share the fixtures and trait list between the preview and the test. The canvas itself is for looking; the test is the record you keep in CI. Keep the set small and review diffs on purpose.
How do you preview both light and dark mode?
Use .preferredColorScheme(.dark) on a named preview, and keep a default (light) preview as well. Check contrast and hardcoded colours in both. You can also flip the canvas scheme in Xcode, but named previews are more reliable as documentation of what you actually looked at.
Written by Lee Johnson. Published 10 November 2026.