swiftuiswiftdata

Previewing SwiftData Records in Xcode Previews


I am using the following code to preview a view.

#Preview {
    
    @Previewable @Query var vegetables: [Vegetable]
    
    NavigationStack {
        VegetableDetailScreen(vegetable: vegetables[0])
    }.modelContainer(previewContainer)
}

But when I run this I get index out of range. This maybe because when the VegetableDetailScreen is rendered then at that time vegetables[0] has not loaded yet.

How can I make sure that vegetables are loaded before VegetableDetailScreen screen?

My previewContainer looks like this:

@MainActor
let previewContainer: ModelContainer = {
    
    do {
        
        let container = try ModelContainer(for: Vegetable.self, MyGardenVegetable.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
        
        let vegetables = PreviewData.loadVegetables()
        for vegetable in vegetables {
            let vegetableModel = Vegetable(vegetableDTO: vegetable)
            container.mainContext.insert(vegetableModel)
        }
        
        return container
        
    } catch {
        fatalError("Failed to create container.")
    }
}()

UPDATE:

If I add if let then the view never gets rendered.

#Preview {
    
    @Previewable @Query var vegetables: [Vegetable]
    
    NavigationStack {
        if let vegetable = vegetables.first {
            VegetableDetailScreen(vegetable: vegetable)
        }
    }.modelContainer(previewContainer)
} 

Solution

  • The @Query in this case can't actually see the model container added by .modelContainer(previewContainer). @Previewable wraps everything into its own view:

    Tagging a variable declaration at root scope in your #Preview body with ‘@Previewable’ allows you to use dynamic properties inline in previews. The #Preview macro will generate an embedded SwiftUI view; tagged declarations become properties on the view, and all remaining statements form the view’s body.

    Expanding the #Preview macro, the code looks like this:

    static func makePreview() throws -> DeveloperToolsSupport.Preview {
        DeveloperToolsSupport.Preview {
            struct __P_Previewable_Transform_Wrapper: SwiftUI.View {
                @Query var vegetables: [Vegetable]
                
                var body: some SwiftUI.View {
                    NavigationStack {
                        VegetableView(vegetable: vegetables[0])
                    } .modelContainer(previewContainer)
                }
            }
            return __P_Previewable_Transform_Wrapper()
            
        }
    }
    

    As you can see, modelContainer modifies the NavigationStack only, instead of modifying the __P_Previewable_Transform_Wrapper, so @Query doesn't know about the model container.


    You can instead inject the model container using a PreviewModifier. Here is an example, adapted from the one shown in the documentation page.

    struct SampleData: PreviewModifier {
        // make your previewContainer here
        static func makeSharedContext() throws -> ModelContainer {
            let container = try ModelContainer(for: Vegetable.self, configurations: ModelConfiguration(isStoredInMemoryOnly: true))
            container.mainContext.insert(Vegetable(name: "Lettuce"))
            return container
        }
    
    
        func body(content: Content, context: ModelContainer) -> some View {
            content.modelContainer(context)
        }
     }
    
    // PreviewModifier is not available before these versions, but since this is preview,
    // this works even if you need to support earlier versions
    @available(iOS 18, macOS 15, *)
    #Preview(traits: .modifier(SampleData())) {
        @Previewable @Query var vegetables: [Vegetable]
        
        NavigationStack {
            VegetableView(vegetable: vegetables[0])
        }
    }
    
    // ---- Model class and VegetableView ----
    
    @Model
    class Vegetable {
        var name = ""
        
        init(name: String = "") {
            self.name = name
        }
    }
    
    struct VegetableView: View {
        let vegetable: Vegetable
        
        var body: some View {
            Text(vegetable.name)
        }
    }