SwiftUI Sheet中如何将上下文连接到持久化存储协调器?
应用背景
我正在开发一款名为Interval的SwiftUI应用,使用CoreData存储Workout与其Step的一对多关系,每个Workout包含多个Step。
视图层级
- ContentView -> DetailView -> EditView(以Sheet形式弹出)
- ContentView展示所有Workout列表,点击条目打开DetailView显示对应Workout的所有Step
- DetailView中点击工具栏按钮弹出EditView Sheet,用于添加、删除或重新排序当前Workout的Step
遇到的问题
为了让列表的.onDelete、.onMove功能兼容CoreData,我在EditView中用FetchRequest获取当前Workout的所有Step,但打开EditView时会弹出警告:
"环境中的上下文未连接到持久化存储协调器: <NSManagedObjectContext: 0x6000014ccea0>"
原本以为Sheet会自动继承环境的上下文,但实际没有生效,尝试手动在Sheet中传递environment(\.managedObjectContext, moc)也没用。
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 { // 展示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) 尝试过但无效 } } } }
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 // 展示step内容 } // .onDelete() // .onMove() } header: { Text("Steps") } } .navigationTitle("Edit workout") .toolbar { ToolbarItem(placement: .confirmationAction) { Button { // 保存操作 dismiss() } label: { Text("Save") } } } } }
问题根源在于EditView初始化时创建FetchRequest的时机过早:此时SwiftUI还未将环境上下文注入到视图中,导致FetchRequest使用了未连接到持久化存储的默认上下文。
解决方法很简单,在创建FetchRequest时,直接传入当前Workout所在的上下文(NSManagedObject自带managedObjectContext属性),而不是依赖环境注入:
修改EditView的init方法:
init(workout: Workout) { self.workout = workout // 直接使用workout的上下文创建FetchRequest guard let moc = workout.managedObjectContext else { fatalError("Workout must be attached to a managed context") } _steps = FetchRequest( entity: Step.entity(), sortDescriptors: [ NSSortDescriptor(keyPath: \Step.index, ascending: true) ], predicate: NSPredicate(format: "workout == %@", workout), context: moc // 关键:指定上下文 ) }
这样修改后,FetchRequest会直接使用Workout所在的有效上下文,无需依赖环境传递,警告会消失,同时.onDelete和.onMove功能也能正常工作。
另外,你也可以移除EditView中@Environment(\.managedObjectContext) var moc这一行,因为现在已经通过Workout获取了上下文。
内容的提问来源于stack exchange,提问作者workbytaylor

