@ArneGockeln

How to use SwiftData in Preview Macro

Written by Arne in software , tagged with SwiftData, SwiftUI, Xcode


I am building a macOS app which relies heavily on SwiftUI and SwiftData in Xcode 15.0 on macOS Sonoma 14.0. While building the views I wanted to be able to create demo data in SwiftData which I can see and interact with in the Xcode preview without building the app. But also when I build and run the app I want to see demo data ready to use. Turns out that this is quite simple to implement.

To make things easier to follow, I’ve created this little demo application and published it to GitHub. You can find the repository here. The App handles StarTrek ships and its spaceship parts. 😎

Xcode Preview Demo Data

The concept

Basically I created a singleton data class to manage the creation of a single ModelContainer and ModelContext to use in Xcode preview and in the production version of the app. Every view that needs demo data relies on the same shared ModelContainer, but in preview mode the data is stored only in memory. This way it’s possible to create even different previews for light and dark mode for example, and still use the same ModelContainer.

DataController Singleton

I created a DataController singleton to handle the creation of a ModelContext and demo data. The interesting part is that I create demo data only if Xcode build and runs in demo mode. This is seen in the file DataController.swift on line 36-43 with the conditional #if DEBUG macro:

/// Create a ModelContainer to use in memory for preview or store in production
/// - Parameter inMemory: Set to true to store in memory
/// - Returns: ModelContainer for Spaceship and SpaceshipPart
public func sharedModelContainer(inMemory: Bool = false) -> ModelContainer {
    if self.modelContainer != nil {
        return self.modelContainer!
    }
    
    do {
        let schema = Schema([Spaceship.self, SpaceshipPart.self])
        let config = ModelConfiguration(schema: schema, isStoredInMemoryOnly: inMemory)
        self.modelContainer = try ModelContainer(for: schema, configurations: [config])
        
        /// Create demo data when running in Xcode debug mode (build and run)
        #if DEBUG
        do {
            Task {
                @MainActor in
                DataController.shared.createShips()
            }
        }
        #endif
        
        return self.modelContainer!
    } catch {
        fatalError("Failed to create \(inMemory ? "previewContainer" : "modelContainer") for: \(error.localizedDescription)")
    }
}

createShips() is a private function which is only called if the ModelContainer get’s initialized. We don’t want to have duplicates.

Init the App

The ModelContainer is injected in the file SwiftDataPreviewApp.swift starting in line 17 with the conditional #if DEBUG macro:

import SwiftUI
import SwiftData

@main
struct SwiftDataPreviewApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        #if DEBUG /// Running in Debug mode in Xcode sets always to store in memory
        .modelContainer(DataController.shared.sharedModelContainer(inMemory: true))
        #else
        .modelContainer(DataController.shared.sharedModelContainer())
        #endif
    }
}

Using the preview macro in ContentView

Now in ContentView.swift I create the main view for the app. It has a NavigationSplitView which lists the spaceship names. When selected it loads another view SpaceshipView to show the ship name and to list the attached spaceship parts. Very basic stuff so far.

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) 
    private var modelContext
    
    @Query(sort: \Spaceship.name, order: .forward)
    private var ships: [Spaceship]
    
    @State
    private var selectedShip: Spaceship? = nil
    
    var body: some View {
        NavigationSplitView {
            /// List all ships in sidebar
            List(selection: $selectedShip) {
                ForEach(ships, id: \.id) { ship in
                    NavigationLink(ship.name, value: ship)
                }
            }
        } detail: {
            if let selectedShip {
                /// List ship parts
                SpaceshipView(ship: selectedShip)
            } else {
                Text("Select a ship")
            }
        }
        .onAppear {
            if selectedShip == nil {
                selectedShip = ships.first
            }
        }
    }
}

#Preview("Light") {
    /// Create the in memory container
    let container = DataController.shared.sharedModelContainer(inMemory: true)
    /// Return view with custom container
    return ContentView()
        .modelContainer(container)
        .preferredColorScheme(.light)
}

#Preview("Dark") {
    /// Create the in memory container
    let container = DataController.shared.sharedModelContainer(inMemory: true)
    /// Return view with custom container
    return ContentView()
        .modelContainer(container)
        .preferredColorScheme(.dark)
}

Starting in line 45 I define the preview macro for the light mode version:

#Preview("Light") {
    /// Create the in memory container
    let container = DataController.shared.sharedModelContainer(inMemory: true)
    /// Return view with custom container
    return ContentView()
        .modelContainer(container)
        .preferredColorScheme(.light)
}

I am getting the shared ModelContainer and pass it to the ContentView. For the dark mode I just duplicated the preview macro, set the name to “Dark” and set the preferredColorScheme to .dark.

Using the preview macro in SpaceshipView

In the single SwiftUI view SpaceshipView I am creating another #Preview macro to show only the single view. But I want to see the same data. So I created another function createPreviewShip in DataController. I am passing the ModelContainer to that function to create inside the context.

/// Create a single spaceship for preview
/// - Parameter modelContext: The ModelContext to create the data in
/// - Returns: A Spaceship object with parts attached
public func createPreviewShip(in modelContext: ModelContext) -> Spaceship {
    let ship = Spaceship(name: "Enterprise NX-01")
    modelContext.insert(ship)
    
    let parts = DataController.shared.parts
    for name in parts {
        /// Create part by name
        let part = SpaceshipPart(name: name)
        /// Add to model context
        modelContext.insert(part)
        
        /// Add relationships
        part.ships.append(ship)
    }
    
    return ship
}

Then in the file SpaceshipView.swift I am creating the #Preview macro and pass the newly created ship to the view.

import SwiftUI
import SwiftData

struct SpaceshipView: View {
    var ship: Spaceship
    
    var body: some View {
        VStack(alignment: .leading, spacing: 5) {
            Text(ship.name)
                .font(.title)
            
            List(ship.parts) { part in
                Text(part.name)
                    .font(.subheadline)
            }
        }
    }
}

#Preview {
    /// Get or create the container in memory
    let container = DataController.shared.sharedModelContainer(inMemory: true)
    /// Create a preview ship in mainContext
    let ship = DataController.shared.createPreviewShip(in: container.mainContext)
    /// Preview the view
    return SpaceshipView(ship: ship)
}

Xcode Preview Macro used in single SwiftUI view

That’s it. Super simple and clean example to use the preview macro in SwiftUI with SwiftData. I hope you find this useful.

Top