andrw
andrw

Reputation: 810

Passing Core Data FetchedResults<T> for previews in SwiftUI

I have a parent view which does a @FetchRequest and passes the FetchedResults<T> to a child view. Everything works, and the child view is able to parse through the FetchedResults. However, I can't figure out how to set up the data so that the child's Preview struct will work. What's the proper way to set up some constant data in Preview struct so that I can instantiate the child view and pass in FetchedResults<T>?

Upvotes: 13

Views: 5505

Answers (3)

tylermilner
tylermilner

Reputation: 516

I came across this use case as well. In my example, I have a MoviesListView, which is a child view of my ContentView. My ContentView utilizes the @FetchRequest property wrapper to automatically load the movies from Core Data and then provides the FetchedResults<Movie> to the MoviesListView child view:

struct ContentView: View {
    
    @Environment(\.managedObjectContext) private var moc
    @FetchRequest(sortDescriptors: []) private var movies: FetchedResults<Movie>
    
    var body: some View {
        NavigationView {
            MoviesListView(movies: movies)
                .navigationTitle("Movies")
        }
    }
}

Originally, I was hoping I could just let previewMovies = FetchedResults<Movie>() to create a list of movies in the preview and pass it into the child view, but there doesn't appear to be an initializer publicly available for FetchedResults<T>.

To solve this, I implemented a solution provided by Azam Sharp, which is to create a lightweight container view inside of the preview that will perform the same duties of the content view to fetch the movies from Core Data and provide them to the child view. Here is what that preview code looks for my MoviesListView child view:

struct MoviesListView: View {

    let movies: FetchedResults<Movie>
    
    var body: some View {
        List {
            ForEach(movies) { movie in
                NavigationLink {
                    MovieDetailView(movie: movie)
                } label: {
                    Text(movie.title)
                }
            }
        }
    }
}

struct MoviesListView_Previews: PreviewProvider {
    
    struct MoviesListViewContainer: View {
        @FetchRequest(sortDescriptors: []) private var movies: FetchedResults<Movie>
        
        var body: some View {
            MoviesListView(movies: movies)
        }
    }
    
    static var previews: some View {
        MoviesListViewContainer()
            .environment(\.managedObjectContext, PreviewDataController.moc)
    }
}

The final piece of the puzzle is my PreviewDataController, which sets up an in-memory only Core Data store for my SwiftUI previews, based on this post.

Overall, I like this solution better than the one provided by Asperi because it doesn't require me to modify or restructure my child view for the sake of making it work nicely with SwiftUI Previews. The container view code is also neatly tucked away inside of the preview itself, which means it's not in the same realm as my production code.

Final Note: I paired down these code samples from my actual code so I make no guarantees on their syntax correctness! 🙂

Upvotes: 2

Asperi
Asperi

Reputation: 257693

As FetchedResults<T> is a RandomAccessCollection and swift array also is a RandomAccessCollection, here is possible solution.

Update: verified with Xcode 13.3 / iOS 15.4

struct ContentView: View {
    @Environment(\.managedObjectContext) var context
    @FetchRequest(entity: Person.entity(), sortDescriptors: [])
        var result: FetchedResults<Person>

    var body: some View {
        VStack(alignment: .leading) {
            Text("Persons").font(.title)
            PersonsView(results: result) // FetchedResults<Person> is a collection
        }
    }
}

// generalize PersonsView to depend just on collection
struct PersonsView<Results:RandomAccessCollection>: View where Results.Element == Person {
    let results: Results
    var body: some View {
        ForEach(results, id: \.self) { person in
            Text("Name: \(person.name ?? "<unknown>")")
        }
    }
}

// Tested with Xcode 11.4 / iOS 13.4
// DOES NOT WORK ANYMORE !!
// struct ChildView_Previews: PreviewProvider {
//    static var previews: some View {
//        PersonsView(results: [Person()]) // << use regular array //to test
//    }
}

Update: fixed & tested part for Xcode 12 / iSO 14 (due to crash of above PreviewProvider)

It appears entity now should be read & specified explicitly:

struct ChildView_Previews: PreviewProvider {
    static let entity = NSManagedObjectModel.mergedModel(from: nil)?.entitiesByName["Person"]

    static var previews: some View {
        let person = Person(entity: entity!, insertInto: nil)
        person.name = "Test Name"

        return PersonsView(results: [person])
    }
}

Upvotes: 13

user14534957
user14534957

Reputation: 408

Use the preview PersistenceController within your pre-generated PersistenceController struct (inside the "Persistence.swift" file).

so if you pass an item from a Core Data "Item"-entity:

struct ContentView: View {
...
private var items: FetchedResults<Item>
..
ForEach(items) { item in
DetailView(item: item)
}
..

In the Detail-View go like this:


struct DetailView: View {
    var item: FetchedResults<Item>.Element
    
    var body: some View {
        Text("Items text = \(item.text ?? "")")
    }
}

struct Detail_Previews: PreviewProvider {

    static var previews: some View {
        let viewContext = PersistenceController.preview.container.viewContext
        let previewItem = Item(context: viewContext)
        previewItem.text = "Text4preview"
        
        return Detail(item: previewItem)
    }
}

Upvotes: 8

Related Questions