Preview crashing in custom view using SwiftData model

So I have two class models, Item and Category, as follows:

@Model
class Category {
    let name: String
    ...
}

@Model
class Item {
    var name: String
    var category: Category?
    ... 
}

I have a view called ItemDetailsView that displays all the properties of a given item. It also has a toggle that shows an ItemEditorView where user's can edit the item properties, including the category which shows up in a picker.

I set up the preview for ItemDetailsView as follows:

#Preview {
    let preview = Preview(Item.self, Category.self)
    preview.addExamples(Category.sampleCategories)
    preview.addExamples(Item.sampleItems)
    return ItemDetailsView(item: Item.sampleItems[0])
        .modelContainer(preview.container)
}

The Preview class sets up my model container and inserts the sample items and categories that I have instantiated as static constant variables.

struct Preview {
    let container: ModelContainer
    
    init(_ models: any PersistentModel.Type...) {
        let schema = Schema(models)
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        do {
            container = try ModelContainer(for: schema, configurations: config)
        } catch {
            fatalError("Could not create preview container")
        }
    }
    
    func addExamples(_ examples: [any PersistentModel]) {
        Task { @MainActor in
            examples.forEach { example in
                container.mainContext.insert(example)
            }
        }
    }
}

I'm not sure why my preview is crashing. I had a hunch that it was because we needed all the categories to be available for the picker in ItemEditorView but I already inserted the relevant categories into the model context. Preview is working as expected in a different view called ItemDetailsView which lists all the items added. Navigation links and sheets work perfectly. Not sure why this is giving me trouble.

Answered by DTS Engineer in 800575022

@randallle I suspect you might need to create a view to use only in previews that creates a model container before showing the preview content. This view would create the model container before displaying the preview.

I would suggest you review Adding and editing persistent data in your app sample project particularly the Preview+ModelContainer.swift and ModelContainerPreview.swift source file.

However, here's an example:

struct ModelContainerPreview<Content: View>: View {
    var content: () -> Content
    let container: ModelContainer

    /// Creates an instance of the model container preview.
    ///
    /// This view creates the model container before displaying the preview
    /// content. The view is intended for use in previews only.
    ///
    ///     #Preview {
    ///         ModelContainerPreview {
    ///             AnimalEditor(animal: nil)
    ///                 .environment(NavigationContext())
    ///             } modelContainer: {
    ///                 let schema = Schema([AnimalCategory.self, Animal.self])
    ///                 let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
    ///                 let container = try ModelContainer(for: schema, configurations: [configuration])
    ///                 Task { @MainActor in
    ///                     AnimalCategory.insertSampleData(modelContext: container.mainContext)
    ///                 }
    ///             return container
    ///         }
    ///     }
    ///
    /// - Parameters:
    ///   - content: A view that describes the content to preview.
    ///   - modelContainer: A closure that returns a model container.
    init(@ViewBuilder content: @escaping () -> Content, modelContainer: @escaping () throws -> ModelContainer) {
        self.content = content
        do {
            self.container = try MainActor.assumeIsolated(modelContainer)
        } catch {
            fatalError("Failed to create the model container: \(error.localizedDescription)")
        }
    }

    /// Creates a view that creates the provided model container before displaying
    /// the preview content.
    ///
    /// This view creates the model container before displaying the preview
    /// content. The view is intended for use in previews only.
    ///
    ///     #Preview {
    ///         ModelContainerPreview(SampleModelContainer.main) {
    ///             AnimalEditor(animal: .kangaroo)
    ///                 .environment(NavigationContext())
    ///         }
    ///     }
    ///
    /// - Parameters:
    ///   - modelContainer: A closure that returns a model container.
    ///   - content: A view that describes the content to preview.
    init(_ modelContainer: @escaping () throws -> ModelContainer, @ViewBuilder content: @escaping () -> Content) {
        self.init(content: content, modelContainer: modelContainer)
    }

    var body: some View {
        content()
            .modelContainer(container)
    }
}

@randallle I suspect you might need to create a view to use only in previews that creates a model container before showing the preview content. This view would create the model container before displaying the preview.

I would suggest you review Adding and editing persistent data in your app sample project particularly the Preview+ModelContainer.swift and ModelContainerPreview.swift source file.

However, here's an example:

struct ModelContainerPreview<Content: View>: View {
    var content: () -> Content
    let container: ModelContainer

    /// Creates an instance of the model container preview.
    ///
    /// This view creates the model container before displaying the preview
    /// content. The view is intended for use in previews only.
    ///
    ///     #Preview {
    ///         ModelContainerPreview {
    ///             AnimalEditor(animal: nil)
    ///                 .environment(NavigationContext())
    ///             } modelContainer: {
    ///                 let schema = Schema([AnimalCategory.self, Animal.self])
    ///                 let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
    ///                 let container = try ModelContainer(for: schema, configurations: [configuration])
    ///                 Task { @MainActor in
    ///                     AnimalCategory.insertSampleData(modelContext: container.mainContext)
    ///                 }
    ///             return container
    ///         }
    ///     }
    ///
    /// - Parameters:
    ///   - content: A view that describes the content to preview.
    ///   - modelContainer: A closure that returns a model container.
    init(@ViewBuilder content: @escaping () -> Content, modelContainer: @escaping () throws -> ModelContainer) {
        self.content = content
        do {
            self.container = try MainActor.assumeIsolated(modelContainer)
        } catch {
            fatalError("Failed to create the model container: \(error.localizedDescription)")
        }
    }

    /// Creates a view that creates the provided model container before displaying
    /// the preview content.
    ///
    /// This view creates the model container before displaying the preview
    /// content. The view is intended for use in previews only.
    ///
    ///     #Preview {
    ///         ModelContainerPreview(SampleModelContainer.main) {
    ///             AnimalEditor(animal: .kangaroo)
    ///                 .environment(NavigationContext())
    ///         }
    ///     }
    ///
    /// - Parameters:
    ///   - modelContainer: A closure that returns a model container.
    ///   - content: A view that describes the content to preview.
    init(_ modelContainer: @escaping () throws -> ModelContainer, @ViewBuilder content: @escaping () -> Content) {
        self.init(content: content, modelContainer: modelContainer)
    }

    var body: some View {
        content()
            .modelContainer(container)
    }
}

Accepted Answer

I actually managed to get it to work in a different way. Before, I was trying to access the static Item variables itself, but what I should have done is fetch from the model context after I inserted the static Item objects into my model.

#Preview {
    let preview = Preview(Item.self, Category.self)
    preview.addExamples(Category.sampleCategories)
    preview.addExamples(Item.sampleItems)

    // SOLUTION
    let samples = preview.getSamples(Item.self)
    return NavigationStack {
        ItemDetailsView(item: samples[0])
            .modelContainer(preview.container)
}

where getSamples() is a generic function defined in the Preview class as

    @MainActor
    func getSamples<T: PersistentModel>(_ model: T.Type) -> [T] {
        let context = container.mainContext
        let request = FetchDescriptor<T>()
        
        do {
            let samples = try context.fetch(request)
            return samples
        } catch {
            print("Failed to fetch model: \(error)")
            return []
        }
    }

Not sure if this is the "proper" way of doing it, but it worked.

Preview crashing in custom view using SwiftData model
 
 
Q