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)
}
}
Apple's solution is a bit complex.
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.
(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! 😃
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])
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With