Reusable SwiftUI Previews with PreviewModifier


Greetings, traveler!

SwiftUI previews often start small, then fill up with repeated setup code. Several screens may need the same mock services, sample data, or in-memory ModelContainer.

PreviewModifier moves that setup into a reusable preview environment. The preview system can then share the prepared context across participating previews that use the same modifier type.

What is PreviewModifier?

PreviewModifier is a preview API introduced with Xcode 16. It separates dependency setup from the #Preview declaration.

A modifier can create shared context and apply it to the preview content:

struct SampleDataPreviewModifier: PreviewModifier {
    static func makeSharedContext() throws -> ModelContainer {
        let configuration = ModelConfiguration(
            isStoredInMemoryOnly: true
        )

        let container = try ModelContainer(
            for: Item.self,
            configurations: configuration
        )

        let context = container.mainContext

        context.insert(Item(title: "Milk"))
        context.insert(Item(title: "Coffee"))
        context.insert(Item(title: "Bread"))

        try context.save()

        return container
    }

    func body(
        content: Content,
        context: ModelContainer
    ) -> some View {
        content.modelContainer(context)
    }
}

The preview stays focused on the view:

#Preview(
    traits: .modifier(SampleDataPreviewModifier())
) {
    ShoppingListView()
}

Creating shared context

A modifier that needs shared data implements makeSharedContext():

static func makeSharedContext() async throws -> Context

The method creates the value that body(content:context:) later applies to the preview.

The context can contain:

  • an in-memory model container
  • mock services
  • fixture data
  • an application dependency container
  • data loaded from a local file

The method supports asynchronous work and error propagation.

The preview system caches the returned context and passes it to each participating preview that applies a modifier of the same type. This prevents every preview from repeating the same setup work.

The context is shared, so it is better suited to dependencies and seed data than to preview-specific mutable state.

Applying the context

SwiftUI passes the prepared context into the modifier’s body method:

func body(
    content: Content,
    context: AppDependencies
) -> some View {
    content
        .environment(context.session)
        .environment(context.apiClient)
}

content is the view returned by #Preview. The method can apply regular SwiftUI modifiers, inject environment values, or attach a model container.

When several dependencies belong together, a dedicated context type keeps them organized:

struct AppDependencies {
    let session: SessionModel
    let apiClient: MockAPIClient
}

Creating a reusable preview trait

Using .modifier(...) directly is valid, but a custom trait keeps repeated previews shorter.

extension PreviewTrait where T == Preview.ViewTraits {
    @MainActor
    static var sampleData: Self {
        .modifier(SampleDataPreviewModifier())
    }
}

The modifier can now be applied by name:

#Preview(traits: .sampleData) {
    ShoppingListView()
}

A larger project can define several focused environments:

#Preview(traits: .emptyDatabase) {
    ShoppingListView()
}

#Preview(traits: .authenticatedUser) {
    ProfileView()
}

Each trait should describe a specific preview scenario rather than becoming a single container for every dependency in the application.

A context is optional

PreviewModifier does not always need shared data. Its context can remain Void when the modifier only applies view configuration.

struct AccessibilityPreviewModifier: PreviewModifier {
    func body(
        content: Content,
        context: Void
    ) -> some View {
        content
            .environment(
                \.dynamicTypeSize,
                .accessibility3
            )
    }
}

This is useful for reusable localization, color scheme, accessibility, or environment configurations.

Using PreviewModifier with @Previewable

PreviewModifier and @Previewable handle different kinds of state.

PreviewModifier provides shared dependencies. @Previewable creates local state for one preview.

#Preview(traits: .sampleData) {
    @Previewable @State var isPresented = false

    ShoppingListView(
        isPresented: $isPresented
    )
}

The model container comes from the shared modifier, while isPresented belongs only to this preview.

This distinction matters when the shared context contains reference types. Mutating a shared object in one preview may affect another participating preview that uses the same modifier type.

When to use PreviewModifier

PreviewModifier is useful when several previews need the same setup:

  • SwiftData or Core Data containers
  • mock networking
  • environment models
  • dependency injection containers
  • local fixtures
  • localization and accessibility settings

A simple preview does not need one:

#Preview {
    BadgeView(title: "New")
}

Once setup code starts appearing in multiple previews, a reusable modifier keeps the configuration in one place and leaves each #Preview focused on the UI state being tested.

Final thoughts

PreviewModifier provides a clear boundary between preview setup and preview content. It reduces duplicated configuration, supports asynchronous preparation, and lets the preview system share expensive context across participating previews.

It is especially useful for SwiftData, mock dependencies, and consistent preview environments across a large project.