swiftui · tutorial · architecture · environment

Preview Dependency Injection Workflow

Published August 31, 2026 · 3 min read · intermediate

Goal

By the end of this tutorial, you will have a repeatable preview setup that injects the same feature dependencies your live screen uses. That makes previews more honest and makes missing dependencies obvious earlier.

Requirements

  • Basic SwiftUI familiarity
  • Xcode 15+
  • A screen that reads at least one environment value or injected service

The use case

This pattern appears when a feature relies on a theme flag, analytics service, or app-specific configuration. Without a workflow, previews often drift away from the real screen because they rely on accidental defaults.

Step 1 - Start with the screen boundary

Keep the feature root responsible for wiring dependencies.

import SwiftUI

struct FeatureScreen: View {
    @Environment(\.isProFeatureEnabled) private var isProFeatureEnabled

    var body: some View {
        VStack(spacing: 12) {
            Text(isProFeatureEnabled ? "Pro mode" : "Free mode")
            Text("The screen reads dependencies at the boundary.")
        }
        .padding()
    }
}

What changed:

  • the screen reads the dependency where it is used
  • the boundary stays explicit

Verification:

  • the screen still compiles
  • the behavior changes when the environment value changes

Step 2 - Add a preview that mirrors reality

Inject the dependency in the preview instead of relying on defaults.

#Preview("Pro") {
    FeatureScreen()
        .environment(\.isProFeatureEnabled, true)
}

#Preview("Free") {
    FeatureScreen()
        .environment(\.isProFeatureEnabled, false)
}

What changed:

  • the preview now documents the dependency
  • you can inspect both states without changing app code

Verification:

  • both previews render
  • the special state is visible immediately

Step 3 - Make the setup reusable

If the same feature needs the same injected state in many previews, extract a helper.

struct FeaturePreviewWrapper<Content: View>: View {
    let isProFeatureEnabled: Bool
    let content: () -> Content

    var body: some View {
        content()
            .environment(\.isProFeatureEnabled, isProFeatureEnabled)
    }
}

What changed:

  • the dependency wiring is centralized
  • feature previews stay short and readable

Verification:

  • the wrapper still passes the same value to the screen
  • changing the helper changes all previews consistently

Result

You now have a preview workflow that mirrors production more closely. That makes feature extraction safer because the dependency wiring is visible instead of hidden in app defaults.

Common SwiftUI pitfalls

  • Mistake: previewing a screen with no environment injection
    Why it happens: the default happens to be good enough
    How to fix it: make the preview state explicit
  • Mistake: putting service logic directly into the preview
    Why it happens: it feels faster at first
    How to fix it: keep previews declarative and pass in values instead

When NOT to use this

Avoid the workflow if the screen does not depend on injected state. Do not add a helper just to avoid one line of preview setup.

Takeaway

If you remember one thing: A good preview makes dependencies obvious, not optional.

Next steps

This is the right continuation because it keeps the same ownership and environment boundaries visible in the surrounding SwiftUI architecture posts: