How to use SwiftData in Preview Macro
Written by Arne in development , 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. 😎
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)
}
That’s it. Super simple and clean example to use the preview macro in SwiftUI with SwiftData. I hope you find this useful.