Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftData Preview Crash

I've created a basic database model in SwiftData to show an issue I'm encountering; a parent class and child class, with a relationship.

This is displayed in a simple view.

When I try and preview this in the canvas in Xcode, it crashes. If I comment out the item.parent = .preview, it doesn't crash.

Everything below (and including) the actor PreviewSampleData { is based on Apple's SwiftData code examples.

Any help would be greatly appreciated. Many thanks.

import SwiftUI
import SwiftData

struct ContentView: View {
    var child: Child
    
    var body: some View {
        VStack {
            Text("\(child.name)")
            Text("\(child.parent?.name ?? "No name")")
        }
        .padding()
    }
}

#Preview {
    ModelContainerPreview(PreviewSampleData.inMemoryContainer) {
        ContentView(child: .preview)
    }
}

@Model final class Parent {
    var name: String
    
    @Relationship(deleteRule: .cascade, inverse: \Child.parent)
    var children: [Child] = [Child]()
    
    init(name: String) {
        self.name = name
    }
}
 
extension Parent {
    static var preview: Parent {
        Parent(name: "Parent 1")
    }
}

@Model final class Child {
    var name: String
    var parent: Parent?
    
    init(name: String) {
        self.name = name
    }
}

extension Child {
    static var preview: Child {
        let item = Child(name: "Child 1")
        item.parent = .preview
        return item
    }
}

actor PreviewSampleData {

    @MainActor
    static var container: ModelContainer = {
        return try! inMemoryContainer()
    }()

    static var inMemoryContainer: () throws -> ModelContainer = {
        let schema = Schema([Parent.self, Child.self])
        let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try! ModelContainer(for: schema, configurations: [configuration])
        let sampleData: [any PersistentModel] = [
            Parent.preview, Child.preview
        ]
        Task { @MainActor in
            sampleData.forEach {
                container.mainContext.insert($0)
            }
        }
        return container
    }
}

struct ModelContainerPreview<Content: View>: View {
    var content: () -> Content
    let container: ModelContainer
    
    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)")
        }
    }
    
    init(_ modelContainer: @escaping () throws -> ModelContainer, @ViewBuilder content: @escaping () -> Content) {
        self.init(content: content, modelContainer: modelContainer)
    }

    var body: some View {
        content()
            .modelContainer(container)
    }
}
like image 568
Gizza Gander Avatar asked Oct 19 '25 19:10

Gizza Gander


2 Answers

"I want a single model container I fully populate with test data, so I can use this anywhere in my code."

Apple's solution is a bit complex.

Simple Way to Add Mock Data

Here is another way to create a container and add mock data at the same time:

@Model
class FriendModel {
    var firstName: String
    var lastName: String
    
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

extension FriendModel {
    // Create a static property that returns a ModelContainer
    @MainActor
    static var preview: ModelContainer {
        let container = try! ModelContainer(for: FriendModel.self,
                                            configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        
        container.mainContext.insert(FriendModel(firstName: "Jason", lastName: "Barlow"))
        container.mainContext.insert(FriendModel(firstName: "Jennie", lastName: "Wilkinson"))
        container.mainContext.insert(FriendModel(firstName: "Lauren", lastName: "Brady"))
        container.mainContext.insert(FriendModel(firstName: "Matthew", lastName: "Schultz"))
        
        return container
    }
}

Now this static property is on your model that it creates mock data for.

How to use

Getting previews to work with mock data (Reference: "SwiftData Mastery in SwiftUI" book)

Since preview returns a ModelContainer, you can use it with the modelContainer modifier to power your previews with mock data! 😃

like image 110
Mark Moeykens Avatar answered Oct 22 '25 08:10

Mark Moeykens


You have to save the context to get relationships to work.

This works:

import SwiftUI
import SwiftData

struct ContentView: View {
    var child: Child
    
    var body: some View {
        VStack {
            Text("\(child.name)")
            Text("\(child.parent?.name ?? "No name")")
        }
        .padding()
    }
}

#Preview {
    let schema = Schema([Parent.self, Child.self])
    let configuration = ModelConfiguration(isStoredInMemoryOnly: true)
    let container = try! ModelContainer(for: schema, configurations: [configuration])

    let child = Child.preview
    container.mainContext.insert(child)
    try! container.mainContext.save()
    
    return ContentView(child: child)
}

@Model final class Parent {
    var name: String
    
    @Relationship(deleteRule: .cascade, inverse: \Child.parent)
    var children: [Child] = [Child]()
    
    init(name: String) {
        self.name = name
    }
}
 
extension Parent {
    static var preview: Parent {
        Parent(name: "Parent 1")
    }
}

@Model final class Child {
    var name: String
    var parent: Parent?
    
    init(name: String) {
        self.name = name
    }
}

extension Child {
    static var preview: Child {
        let item = Child(name: "Child 1")
        item.parent = .preview
        return item
    }
}

One thing to keep in mind about your code sample is that you're creating Child.preview in PreviewSampleData but that's not the instance you're passing to ContentView. The one you're passing to ContentView is created inline. It's not memoizing Child.preview or anything like that. That's why I restructured this to be clear that it's saving and passing the same instance.

Btw, this works regardless of how your schema is defined. These all work:

    let schema = Schema([Parent.self, Child.self])
    let schema = Schema([Child.self])
    let schema = Schema([Parent.self])
like image 45
Nick Retallack Avatar answered Oct 22 '25 08:10

Nick Retallack



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!