How do I preview a View that's using @Bindable?
Asked Answered
M

3

2

I have a SwiftUI view that is being passed a binding from a parent view. I would like to preview the subview, but can't. I tried .constant and a @Model object directly. Both ways crash the preview.

Any idea on how I can get this to work properly?

@Model
class FamilyMember: Identifiable {
    @Attribute(.unique) var id: String { name }
    var name = ""

    init(name: String = "") {
        self.name = name
    }
}


struct MemberView: View {
    @Bindable var member: FamilyMember

    var body: some View {
        Text(member.name)
    }
}

#Preview {
    MemberView(member: FamilyMember(name: "Family member"))
}

#Preview {
    MemberView(member: .constant(FamilyMember(name: "Family member")))
}
Mccord answered 1/9, 2023 at 18:54 Comment(0)
H
0

I think this is a bug but with this wrapper you can work around it.

struct SwiftDataPreviewWrapper<Content: View>: View {        
    @ViewBuilder var view: Content
    
    let modelContainer: ModelContainer
    
    init<S>(of type: S.Type, view: () -> Content) where S : PersistentModel {
        do {
            modelContainer = try ModelContainer(for: type, configurations: .init(isStoredInMemoryOnly: true))
            
        } catch {
            fatalError("Could not initialize ModelContainer")
        }
        self.view = view()
    }
    var body: some View {
        view
            .modelContainer(modelContainer)
        
    }
}

Then you can use it with any @Model like this

#Preview {
    SwiftDataPreviewWrapper(of: FamilyMember.self){
        MemberView(member: FamilyMember(name: "Family member"))
    }
    
}

The premise behind the wrapper working is that the model container is created and set before the initialization of the object.

Using the modelContainer ViewModifier causes the same crash that says a container isn’t available.

Hanover answered 1/9, 2023 at 22:41 Comment(0)
R
0

This is the solution that allowed us to use model's with relationships and read/write from SwiftUI previews.

@lorem-ipsum's answer was very helpful but still crashed loading relationships since the model can't just be initialized, it needs to be inserted into a context to avoid crashes.

How to use

#Preview {
    SwiftDataPreviewWrapper { modelContext in
        MyView(
            document: Document(
                title: "Section one",
                sections: [
                    Section(
                        content: "My content text",
                    )
                ]
            ).insert(modelContext) // Important to insert using the provided modelContext
        )
    }
}

Implementation with insert extension

/// Provides a in-memory `ModelContext` to the rendering view so `SwiftData` models can be inserted into a `ModelContext` to avoid crashes the preview.
/// While simple `SwiftData` models can be rendered in a preview without being inserted into a context at all once a `SwiftData` model accesses another
/// model via a relationship, usually a child, the preview will crash if the models do not have a context since that is how relationship models are accessed even
/// when it's all defined in memory up front.
/// Inspired by: https://mcmap.net/q/1166541/-how-do-i-preview-a-view-that-39-s-using-bindable
struct SwiftDataPreviewWrapper<Content: View>: View {
    /// The content which passes in the context to the actual preview view code
    let content: (ModelContext) -> Content
    
    let modelContainer: ModelContainer
    
    init(
        for forTypes: any PersistentModel.Type = /* Suggestion: Set a global default value here for less boilerplate. */ Document.self,
        @ViewBuilder content: @escaping (ModelContext) -> Content
    ) {
        do {
            // Create our model container BEFORE we render anything so it's modelContext can be used right away.
            modelContainer = try ModelContainer(
                for: forTypes,
                configurations: .init(isStoredInMemoryOnly: true)
            )
        } catch {
            fatalError("Could not initialize ModelContainer")
        }
        
        self.content = content
    }
    
    var body: some View {
        content(modelContainer.mainContext)
            .modelContainer(modelContainer)
        
    }
}

extension PersistentModel {
    
    /// A helper method that returns the model after inserting it into `context`.
    /// Useful when we want the model right from inserting since inserting from a context is
    /// more boilerplate code that makes the code less concise to read.
    func insert(_ context: ModelContext) -> Self {
        context.insert(self)
        
        return self
    }
    
}
Relevant answered 17/2, 2024 at 18:41 Comment(0)
M
-1

If only reading, use let instead of @Bindable var which is for read/write, e.g

struct MemberView: View {
    let member: FamilyMember

    var body: some View {
        Text(member.name)
    }
}
Mendacity answered 3/9, 2023 at 14:30 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.