Home / Blog / SwiftUI Preview: productivity multiplier or comfortable illusion?

SwiftUI Preview: productivity multiplier or comfortable illusion?

SwiftUI Preview speeds up design iteration but can hand you false confidence. Notes on how I use it across 12 apps.

The first time I used SwiftUI’s Preview I called it a game changer. Instant render on the canvas, real-time updates, parallel previews across environments. After years of Storyboards and Interface Builder in UIKit, it felt like freedom.

Four years and 12 apps later, my view is more nuanced. Preview is powerful and has some real pitfalls. Here’s what I love, and what I watch for.

What Preview gets right

Instant visual feedback. A code change shows up in the canvas in a second or two. No simulator boot, no build-and-run cycle.

Parametric previews. Different data sets, different color schemes, different device sizes side by side.

#Preview("Small") {
    ProductCard(product: .small)
}

#Preview("Large") {
    ProductCard(product: .large)
}

#Preview("Dark Mode") {
    ProductCard(product: .standard)
        .preferredColorScheme(.dark)
}

You look at one component in five states at once.

Environment injection. Locale, color scheme, accessibility settings, size classes, you can inject all of iOS’s environmental edge cases straight into the preview.

#Preview("Arabic") {
    ContentView()
        .environment(.locale, Locale(identifier: "ar"))
        .environment(.layoutDirection, .rightToLeft)
}

To check whether Arabic or RTL layout is broken in production, you don’t need to switch simulators. You see it in the preview.

What Preview gets wrong

Real device is not Preview. Preview runs the SwiftUI render engine, but rendering on the simulator or a real device can differ. Some animations, gestures, and system interactions don’t work in preview.

UI that works in preview can break on real hardware. Always test in simulator and on a real device before shipping.

The performance illusion. The preview canvas is optimised for render speed, not for real device performance. A LazyVStack with 1,000 items looks smooth in preview but stutters on an iPhone 12. Measure performance on hardware.

Mock data bias. Preview mock data tends to be the happy path. In real data there are edge cases (very long strings, empty arrays, nil fields) that break your layout.

Discipline: include the edge cases in preview too.

#Preview("Empty") {
    ListView(items: [])
}

#Preview("Single") {
    ListView(items: [.mock])
}

#Preview("100 items") {
    ListView(items: (0..<100).map { .mock(id: $0) })
}

#Preview("Long title") {
    ListView(items: [.mock(title: String(repeating: "a", count: 500))])
}

Why Preview crashes

The most frustrating thing about Preview is the random “Preview paused” or “Failed to build preview” messages, with no clear fix.

Common causes:

1. External dependency. If the preview makes an API call, reads a file, or touches a CoreData context, it hangs. Catch network calls and fall back to mock data.

2. Missing environment object. The environment object the parent normally provides isn’t there in the preview. Inject everything manually:

#Preview {
    ChildView()
        .environmentObject(AppState())
        .environment(.modelContext, PreviewData.modelContext)
}

3. Singletons and global state. Code that touches UserDefaults, FileManager, Keychain has side effects in preview. Use protocol-based dependency injection and inject mocks in preview.

4. Async work. Long-running async work inside Task {} locks the preview. Use #if DEBUG with a preview-only fast path.

Mock data discipline

Across 12 apps, the single most valuable practice I’ve adopted: every model type has a mock or preview static property.

extension User {
    static let preview = User(
        id: UUID(),
        name: "Test User",
        email: "test@example.com",
        createdAt: Date()
    )
}

extension Order {
    static let preview = Order(
        id: "ord_001",
        user: .preview,
        total: 15000,
        items: [.preview]
    )
}

In the preview:

#Preview {
    OrderDetailView(order: .preview)
}

One line. Consistent mock data, reused everywhere, maintained in one place.

PreviewProvider vs the #Preview macro

Xcode 15+ replaces the PreviewProvider protocol with the #Preview macro. Cleaner:

// Old (still works)
struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

// New
#Preview {
    ContentView()
}

For a new project, use #Preview. Migrating an existing project isn’t urgent.

Interactive preview

On iOS 17+, you can interact with previews. Tap buttons, sheets open, state changes.

Earlier previews were static. Now you can navigate across screens in the canvas. Big productivity gain.

The trap: using Preview as a test

An anti-pattern I see often: using Preview as a test. “Look, it’s fine in preview, works”, and no snapshot test is written.

Preview is a design tool, not a test. No regression detection, no CI integration. Snapshot tests have to be set up separately (the swift-snapshot-testing library is good).

Environment and trait customisation

Simulate iOS system settings in preview:

#Preview("Large Text") {
    ContentView()
        .environment(.dynamicTypeSize, .accessibility3)
}

#Preview("Voice Over") {
    ContentView()
        .environment(.accessibilityEnabled, true)
}

#Preview("Right to Left") {
    ContentView()
        .environment(.layoutDirection, .rightToLeft)
}

Without testing accessibility, large text, and RTL, you ship bugs to production. Preview catches these.

Multi-device preview

Different devices with preview modifiers:

#Preview("iPhone 15") {
    ContentView()
}

#Preview("iPad Pro") {
    ContentView()
        .previewDevice("iPad Pro (12.9-inch)")
}

#Preview("Apple Watch") {
    ContentView()
        .previewDevice("Apple Watch Ultra 2")
}

For a universal app, you see each device side by side.

Canvas performance tuning

When a big view hierarchy slows the preview down:

  • Put the PreviewProvider outside the big list’s LazyVStack; preview the wrapper view
  • Replace complex subviews with a Color.gray placeholder temporarily
  • Disable animations in preview with .transaction { $0.animation = nil }
  • Use placeholder images instead of async image loading

My actual production workflow

  1. I design the feature preview-first
  2. Prepare the mock data (edge cases included)
  3. Iterate in preview until every state is clean
  4. Smoke test in the simulator
  5. Full feature test on a real device
  6. Snapshot tests (optional, for critical UI)

In this flow Preview handles 60% of the first pass, and the other 40% is fixes from real device testing. Preview saves a lot of time, but real hardware has the final word.

Closing thought

SwiftUI Preview is a strong tool. It’s also partly an illusion. Trusting it blindly ships bugs to production.

Sensible use:
– Include edge cases in your mock data
– Simulate system settings with environment injection
– Don’t skip real device testing
– Guard regressions with snapshot tests

With that discipline, Preview doubles your productivity. Without it, “it was fine in preview” bugs end up in production.

Have a project on this topic?

Leave a brief summary — I’ll get back to you within 24 hours.

Get in touch