Syd Polk
Syd Polk

Reputation: 133

SwiftData passing @Model objects as parameters in SwiftUI

I am trying to work with multiple views in a SwiftData based SwiftUI project. However, when I create a parameter for a SwiftData @Model object in another view, when I create a sample of that object in the #Preview, the compiler starts complaining about not implementing protocols in the @Model object.

Xcode 15 beta 2

I started with a new SwiftData project for iOS. I updated ContentView like this:

import SwiftUI
import SwiftData

struct ContentView: View {
    @Environment(\.modelContext) private var modelContext
    @Query private var items: [Item]
    
    @State private var selection: Item?
    
    var body: some View {
        NavigationSplitView {
            List(selection: $selection) {
                ForEach(items) { item in
                        Text(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))
                }
                .onDelete(perform: deleteItems)
            }
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    EditButton()
                }
                ToolbarItem {
                    Button(action: addItem) {
                        Label("Add Item", systemImage: "plus")
                    }
                }
            }
        } detail: {
            DetailView(selection: selection)
        }
        .navigationSplitViewStyle(.balanced)
    }

    private func addItem() {
        withAnimation {
            let newItem = Item(timestamp: Date())
            modelContext.insert(newItem)
        }
    }

    private func deleteItems(offsets: IndexSet) {
        withAnimation {
            for index in offsets {
                modelContext.delete(items[index])
            }
        }
    }
}

#Preview {
    ContentView()
        .modelContainer(for: Item.self, inMemory: true)
}

and then I created DetailView.swift:

import SwiftUI
import SwiftData

struct DetailView: View {
    var selection: Item?
    
    var body: some View {
        if let item = selection {
            Text("Item at \(item.timestamp, format: Date.FormatStyle(date: .numeric, time: .standard))")
        } else {
            Text("nothing selected")
        }
    }
}

#Preview {
    return DetailView(selection: Item(timestamp: Date()))
        .modelContainer(for: Item.self, inMemory: true)
}

Item.swift is unchanged from how it was generate in the new project:

import Foundation
import SwiftData

@Model
final class Item {
    var timestamp: Date
    
    init(timestamp: Date) {
        self.timestamp = timestamp
    }
}

But when I add the parameter in the #Preview of DetailView, the compiler won't build the project:

/var/folders/9j/8r6qn35j5jjf0gm3crp6gynw0000gn/T/swift-generated-sources/@_swiftmacro_12navSplitView4Item5ModelfMc.swift:1:1 Type 'Item' does not conform to protocol 'PersistentModel'

What do I need to do to pass these @Model objects around as parameters?

Upvotes: 10

Views: 3123

Answers (2)

Christian Schuster
Christian Schuster

Reputation: 184

What helped me as workaround: Above your preview code, create a wrapper view like this.

struct Wrapper: View {
    var body: some View {
        EditingView(item: .previewItem)
    }
}

In your preview, just call the Wrapper view and provide a container.

#Preview {
  Wrapper()                
  .modelContainer(previewContainer) // Inject preview content
}

Upvotes: 3

Joakim Danielson
Joakim Danielson

Reputation: 52043

The issue is that the Item object you create inside the #Preview macro doesn't belong to a ModelContext instance which generates the error.

To solve this we first need a separate ModelContainer for previews and use that container's model context to insert objects we use in our previews, here is a simple example.

#if DEBUG
@MainActor
let previewContainer: ModelContainer = {
    do {
        let container = try ModelContainer(for: Item.self, ModelConfiguration(inMemory: true))        
        container.mainContext.insert(Item.preview)
        
        return container
    } catch {
        fatalError("Failed to create preview container")
    }
}()
#endif

where Item.preview is a static property defined in Item

@Model
final class Item {
    // existing code...

    static let preview: Item = {
        Item(timestamp: .now)
    }()
}

And then the preview macro is changed to

#Preview {
    DetailView(selection: .preview)
}

Upvotes: 5

Related Questions