swiftuicore-dataswiftui-environmentnspersistentcontainer

How can I connect a context to a persistent store coordinator in a sheet?


App description

I am creating a swiftUI app (called Interval) that uses CoreData to store Workouts and their Steps in a one-to-many relationship. Each Workout has many Steps.

View hierarchy

ContentView -> DetailView -> EditView (as a sheet)

ContentView which shows a list of all Workouts. Tapping on a Workout in the list opens DetailView which shows a list of all steps in the selected workout.

In DetailView, tapping a button in the toolbar opens a sheet, called EditView, that lists all steps in the selected workout and allows the user to add, delete, or re-order steps.

The problem

To ensure list functionality works with CoreData, I am using a fetchRequest in EditView to fetch all steps of the selected workout. So far, I've not been able to maintain list functionality (.onDelete, .onMove) with any other method.

When I open EditView, the following warning pops up:

"Context in environment is not connected to a persistent store coordinator: <NSManagedObjectContext: 0x6000014ccea0>"

How can I remove the warning, and ensure my context is connected to the persistent store coordinator?

I assumed the EditView sheet would inherit the environment and automatically connect to the persistent store container. That doesn't seem to be the case.

My code

IntervalApp

import SwiftUI

@main
struct IntervalApp: App {
    @StateObject var dataController = DataController()
    
    var body: some Scene {
        WindowGroup {
            NavigationStack {
                ContentView()
                    .environment(\.managedObjectContext, dataController.container.viewContext)
            }
        }
    }
}

DataController

import Foundation
import CoreData
import SwiftUI

class DataController: ObservableObject {
    let container = NSPersistentContainer(name: "Interval")
    
    init() {
        container.loadPersistentStores { description, error in
            if let error = error {
                print("CoreData failed to load: \(error.localizedDescription)")
            }
        }
    }
}

ContentView

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) var moc
    @FetchRequest(fetchRequest: Workout.all()) private var workouts
    
    var body: some View {
        List {
            ForEach(workouts) { workout in
                NavigationLink {
                    DetailView(workout: workout)
                } label: {
                    Text(workout.title)
                }
            }
        }
        .navigationTitle("Workouts")
    }
}

DetailView

import SwiftUI

struct DetailView: View {
    @Environment(\.dismiss) var dismiss
    @Environment(\.managedObjectContext) var moc
    @ObservedObject var workout: Workout
    @State private var showSheet: Bool = false
    
    var body: some View {
        List {
            // list steps
        }
        .navigationBarTitle(workout.title)
        .toolbar {
            ToolbarItemGroup(placement: .navigationBarTrailing) {
                Button {
                    showSheet.toggle()
                } label: {
                    Label("Edit", systemImage: "square.and.pencil")
                }
            }
        }
        .sheet(isPresented: $showSheet) {
            NavigationStack {
                EditView(workout: workout)
                    //.environment(\.managedObjectContext, moc) -> this did not work
            }
        }
    }
}

EditView

import SwiftUI
import CoreData

struct EditView: View {
    @Environment(\.dismiss) var dismiss
    @Environment(\.managedObjectContext) var moc
    @ObservedObject private var workout: Workout
    @FetchRequest private var steps: FetchedResults<Step>
    
    init(workout: Workout) {
        self.workout = workout
        _steps = FetchRequest(
            entity: Step.entity(),
            sortDescriptors: [ NSSortDescriptor(keyPath: \Step.index, ascending: true) ],
            predicate: NSPredicate(format: "workout == %@", workout)
        )
    }
    
    var body: some View {
        List {
            Section {
                ForEach(steps) { step in
                    // list steps
                }
                //.onDelete()
                //.onMove()
            } header: {
                Text("Steps")
            }
        }
        .navigationTitle("Edit workout")
        .toolbar {
            ToolbarItem(placement: .confirmationAction) {
                Button {
                    // save
                    dismiss()
                } label: {
                    Text("Save")
                }
            }
        }
    }
}

I have tried passing the environment to the EditView sheet from DetailView like so:

.sheet(isPresented: $showSheet) {
    NavigationStack {
        EditView(workout: workout)
            .environment(\.managedObjectContext, moc) // Adding this did not work
    }
}

...but it did not work.


Solution

  • Move the line of code down 1 line

        .sheet(isPresented: $showSheet) {
            NavigationStack {
                EditView(workout: workout)
            }.environment(\.managedObjectContext, moc)
        }
    

    and

        NavigationStack {
            ContentView()
        }.environment(\.managedObjectContext, dataController.container.viewContext)
    

    Generally you want to inject onto the Stack that way the NavigationLinks have access to the values