Start a project

Article · iOS · SwiftUI · Design QA · 11 min read

Using SwiftUI previews for design QA

SwiftUI previews are easy to treat as a toy: a single happy-path canvas, on one device, in light mode, with an empty view model. Used properly, they are a design QA surface. You pin the traits users actually have — Dynamic Type, dark mode, a small phone and a large one, UK English, a long name — inject sample data, and look at the screen before a tester does. The #Preview macro is table stakes; the workflow is the list of traits you always show, the data you inject, the discipline that keeps previews fast, and the snapshot tests you hang off the same configurations.

On this page 10 sections
  1. Previews as a QA surface, not a toy
  2. Traits you should always show
  3. Sample data and injected services
  4. Light, dark, Dynamic Type and Dynamic Island
  5. Keeping previews fast and reliable
  6. From a preview to a snapshot test
  7. When a preview is the wrong tool
  8. A short team checklist
  9. Key takeaways
  10. 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:

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:

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.

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:

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:

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:

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

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.

All articles · Swift A–Z · Apps

Questions, or an app in mind?

Let's bring your ideas to life. Whether you're envisioning a game-changing Apple Watch app or a transformative iOS experience, I'm here to make it happen.

Start a project