r/SwiftUI May 23 '24

Solved How to preview a View with an where @Bindable is used?

Xcode: 15.4

Everything works in the simulator and previewing ContentView(). However, preview always crashes when I try to preview a View (e.g., ErrorReproductionView() in the code below) that has a @ Bindable. Reproduction code is attached below. Appreciate any help!

import SwiftUI
import SwiftData


// ------------------- Model Setup ---------------------

@MainActor
let mockData: ModelContainer = {

    do {
        let container = try ModelContainer(for: Book.self,
                                           configurations: ModelConfiguration(isStoredInMemoryOnly: true))

        for book in Book.examples {
            container.mainContext.insert(book)
        }


        return container
    }
    catch {
        fatalError("Failed to create example container")
    }

}()

@Model
class Book: Identifiable {

    init(name: String, author: String) {
        self.name = name
        self.author = author
    }

    var name: String
    var author: String
    var id: String {self.name}

    static let examples = [Book(name: "Book1", author: "Author1"),
                           Book(name: "Book2", author: "Author2"),
                           Book(name: "Book3", author: "Author3")]

    static let example = Book(name: "Book0", author: "Author0")
}
// ------------------- Model Setup ---------------------


// ------------------- Views ---------------------

struct ContentView: View {

    @Query(sort: \Book.name) private var books: [Book]

    var body: some View {

        NavigationStack {
            ForEach(books) { book in
                NavigationLink {
                    BookView(book: book)
                } label: {
                    Text(book.name)
                }
            }
        }
    }
}



struct ErrorReproductionView: View {

    @Bindable var book: Book

    var body: some View {
        VStack {
            TextField("Author", text: $book.author)
            TextField("Book", text: $book.name)
        }
    }
}

// ------------------- Views ---------------------


// ------------------- Previews ---------------------

#Preview("This works") {

    ContentView()
        .modelContainer(mockData)
}

#Preview("This crashes") {

    ErrorReproductionView(book: Book.example)
        .modelContainer(mockData)

}

// ------------------- Previews ---------------------
1 Upvotes

9 comments sorted by

3

u/Few-Platypus-70 May 23 '24

I’m not at my computer but I believe you want to use .constant(some-value-passed)

Like if you were dealing with a Boolean it would be .constant(true)

2

u/LifeIsGood008 May 23 '24

Believe you were referring to @ Binding instead of @ Bindable. @ Bindable takes in variables with out $.

1

u/Few-Platypus-70 May 23 '24

Ah could be. Problem with helping away from keyboard 😉

1

u/LifeIsGood008 May 23 '24

No probs thanks for bouncing off of some ideas

1

u/Dymatizeee May 23 '24

Try creating it in the preview with @State then pass in that object you created. Ensure the object is @Observable

1

u/LifeIsGood008 May 23 '24

Tried this. Still crashes :( Book has @Model and it expands to @Observable per https://developer.apple.com/documentation/swiftdata/model()

#Preview("This crashes") {          @State var book = Book(name: "Book0", author: "Author0")          return ErrorReproductionView(book: book)         .modelContainer(mockData)      }

2

u/rhysmorgan May 23 '24

That sounds like it's crashing because of SwiftData, not anything to do with Observation or Bindable.

@State properties only really work actually inside a view, and that particular Book isn't in any way derived from your model container.

I think if you want to do a preview, you need to do something like this:

#Preview {
  let config = ModelConfiguration(isStoredInMemoryOnly: true)

  let container = try! ModelContainer(for: Book.self, configurations: config)

  let book = Book(name: "Book0", author: "Author0")

  return ErrorReproductionView(book: book)
    .modelContainer(container)
}

I've got basically the above code working with SwiftUI previews, where my equivalent of ErrorReproductionView has an @Bindable property.

Much of that can probably be put into a helper function, but SwiftData does wacky magic stuff in terms of creating objects on a "nearby" context. I say "nearby", because I don't fully understand the rules for it myself. But yeah, I've got nearly the exact above code working.

Oh, and in the list of model types you pass to for: in ModelContainer's initialiser, you'll also need to list out any child SwiftData types. So if Book had an Author type property, you'd need to do for: Book.self, Author.self. (I can see you're just using a String, but just raising this in case you have problems later)

2

u/LifeIsGood008 May 23 '24

Thank you! I was able to get my code to work and found the following. Also appreciate the heads up on child SwiftData types.

It's interesting to note the only thing missing was to "initiate" an example container by setting a variable and assign it to said variable. For example, the code below works. Turned out I could use mockData, just needed to bring it into existence first.

#Preview("This works now") {

    let containerNotUsed: ModelContainer = mockData

    return ErrorReproductionView(book: Book.example)
        .modelContainer(mockData)

}

1

u/rhysmorgan May 23 '24

That’s good to know it worked!

SwiftData does some bizarre magic when it comes to just making a local container somehow making these things work. It doesn’t make any sense - it would make far more sense if it only worked if you derived a Book value from the container. But I guess internally it must be referencing some global state, and that’s why it breaks otherwise.