How to pass Core Data object from one SwiftUI view to another through environment property

Viewed 71

First I should say my current code works fine. Here's what I have in my ContentView:

struct ContentView: View {
    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Pokemon.id, ascending: true)],
        animation: .default)
    private var pokedex: FetchedResults<Pokemon>

    var body: some View {
        NavigationStack {
            List(pokedex) { pokemon in
                NavigationLink(value: pokemon) {
                    Text(pokemon.name!.capitalized)
                }
            }
            .navigationTitle("Pokedex")
            .navigationDestination(for: Pokemon.self, destination: { pokemon in
                PokemonDetail(pokemon: pokemon) // NOTICE THIS
            })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
           .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) // ALSO THIS
    }
}

The part to notice is the line inside the .navigationDestination modifier where I am passing the pokemon object to the PokemonDetail view. I don't want to have to do that, and I'll explain why in a bit.

Also notice in the preview I am able to use a sample Pokemon that I created in the PersistenceController specifically for the preview, through the .environment modifier. I want to be able to do this same thing in my PokemonDetail view, which right now looks like this:

struct PokemonDetail: View {
    let pokemon: Pokemon // NOTE: I don't like this property.
    
    var body: some View {
        ZStack {
            Image(pokemon.background)
                .resizable()
                .scaledToFit()
                .shadow(color: .black, radius: 6, x: 0, y: 0)
            
            AsyncImage(url: pokemon.sprite) { image in
                image
                    .resizable()
                    .scaledToFit()
                    .shadow(color: .black, radius: 6, x: 0, y: 0)
            } placeholder: {
                ProgressView()
            }
            .padding(.top, 100)
        }
    }
}

struct PokemonDetail_Previews: PreviewProvider {
    static var previews: some View {
        let context = PersistenceController.preview.container.viewContext
        
        let fetchRequest: NSFetchRequest<Pokemon> = Pokemon.fetchRequest()
        fetchRequest.fetchLimit = 1
        
        let result = try! context.fetch(fetchRequest)
        
        return PokemonDetail(pokemon: result.first!) // NOTE: I want to use the same .environment modifier I used in ContentView instead of having to create this context and fetch request.
    }
}

Notice how throughout this file I am using that pokemon property that was passed from the previous view. And in the preview I have to fetch a pokemon to use.

If I could, I'd much rather do another @FetchRequest here in the PokemonDetail view and just somehow fetch the Pokemon I tapped on from the ContentView.

And I'd also like to use that same .environment modifier in my PokemonDetail preview instead of having to set up a whole new context and fetch request just to fetch a Pokemon for my preview.

One thought I had was to add an @Environment property to the top of my ContentView, and then add it to the PokemonDetail view with an .environment modifier, like this:

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext // NEW LINE

    @FetchRequest(
        sortDescriptors: [NSSortDescriptor(keyPath: \Pokemon.id, ascending: true)],
        animation: .default)
    private var pokedex: FetchedResults<Pokemon>

    var body: some View {
        NavigationStack {
            List(pokedex) { pokemon in
                NavigationLink(value: pokemon) {
                    Text(pokemon.name!.capitalized)
                }
            }
            .navigationTitle("Pokedex")
            .navigationDestination(for: Pokemon.self, destination: { pokemon in
                PokemonDetail() // CHANGE: pokemon argument was removed
                    .environment(\.managedObjectContext, viewContext) // NEW LINE
            })
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
           .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

But still, in the PokemonDetail, I don't know how I would identify which Pokemon was tapped on so I could show the correct information.

My question is, is this even possible, and if so, how do I do that?

2 Answers

It is possible to do that with .environmentObject modifier, bacause CoreData objects confirm to ObservableObject protocol, so

  1. injected
.navigationDestination(for: Pokemon.self, destination: { pokemon in
    PokemonDetail()
       .environmentObject(pokemon)    // << here !!
})
  1. used
struct PokemonDetail: View {
    @EnvironmentObject var pokemon: Pokemon // << here !!
    
    var body: some View {
// ...
  1. in preview as well
struct PokemonDetail_Previews: PreviewProvider {
    static var previews: some View {

        // ... create preview `pokemon` 

        return PokemonDetail()
            .environmentObject(pokemon)    // << here !!
    }
}

@ObservedObject is like @FetchRequest but it works on one managed object. body is called when any property of the object changes.

struct PokemonDetail: View {
    @ObservedObject var pokemon: Pokemon

You can tidy the code in preview into an extension of NSManagedObjectContext, e.g.

struct PokemonDetail_Previews: PreviewProvider {
    static var previews: some View {
        let context = PersistenceController.preview.container.viewContext
        
        return PokemonDetail(pokemon: context.firstPokemon) 
    }
}

extension NSManagedObjectContext {
    var firstPokemon: Pokemon {
        let fetchRequest = Pokemon.fetchRequest()
        fetchRequest.fetchLimit = 1
        let result = try! fetch(fetchRequest)
        return result.first!
    }

    // put other fetch helpers in here too!
}
Related