如何在 SwiftUI 环境中实现子上下文 (CoreData)?

Lar*_*sen 4 core-data ios swift swiftui

我目前正在学习 SwiftUI,以进一步将其纳入我基于 UIKit 的日常工作流程中。我已经到了无法在 SwiftUI 中运行一个概念的地步,而我已经在 UIKit 中使用了多年。

这个想法是使用我的主 CoreData 的子上下文managedObjectContext来编辑实体。我基本上通过执行以下操作来实现它:

// Get view context of application
let viewContext = (UIApplication.shared.delegate as! AppDelegate).persistentContainer.viewContext

// Create NSManagedObjectContext
let editingContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

// Set the parent of the editing context to the main view context
editingContext.parent = viewContext
Run Code Online (Sandbox Code Playgroud)

通过使用单独的,editingContext我可以对其中的实体进行更改,而无需将更改直接进行到我的主上下文。如果用户选择中止更改,我只需重置editingContext.

对于 SwiftUI 中的实现,我选择通过创建自定义来将EditingContext实现为环境对象EnvironmentKey

struct EditingContextKey: EnvironmentKey {
    static let defaultValue: NSManagedObjectContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
}

extension EnvironmentValues {
    var editingContext: NSManagedObjectContext {
        get {
            return self[EditingContextKey.self]
        }
        set {
            self[EditingContextKey.self] = newValue
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

然后editingContext将其添加到我的根视图中:

let contentView = ContentView()
    .environment(\.managedObjectContext, viewContext)
    .environment(\.editingContext, editingContext)
Run Code Online (Sandbox Code Playgroud)

至此一切正常。我还可以editingContext通过在相应视图中调用以下命令来使用它。

@Environment(\.editingContext) var editingContext
Run Code Online (Sandbox Code Playgroud)

但是,一旦我对 中的实体进行更改editingContext并尝试保存editingContext,我就会收到意外错误:

Fatal error: Unresolved error Error Domain=Foundation._GenericObjCError Code=0 "(null)", [:]: file
Run Code Online (Sandbox Code Playgroud)

我已经检查过,编辑实体的上下文是否与editingContext. 情况似乎是这样。我有一种感觉,它可能与 SwiftUI 中的绑定有关,但不知道如何找到解决方案。

有人遇到过类似的问题吗?或者我实现所需功能的基本方法是否错误,并且有更方便的方法来执行此操作?

谢谢你!

Gen*_*ich 5

看看您的方法,我不建议将两个不同的上下文同时注入到单个视图的环境中。最好清楚地了解视图是在主视图上下文区域还是在编辑子上下文区域。如果我们正在使用一个我们知道是由嵌套上下文驱动的视图,那么仅仅关闭该视图就会从内存中清除其结构,破坏其上下文,从而放弃我们的更改。

我们还必须注意创建嵌套上下文的位置。我曾经犯过的错误是在修饰符内创建嵌套上下文,.sheet如下所示:

.sheet(isPresented: $isPresentingItemEditor) {
    makeItemEditor()
}

func makeItemEditor() -> some View {
     let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
     childContext.parent = viewContext
     let childItem = Item(context: childContext)
     return ItemEditor(item: childItem)
         .environment(\.managedObjectContext, childContext)
}
Run Code Online (Sandbox Code Playgroud)

一开始它工作得很好,但系统可能会.sheet多次调用修饰符,并且我们将为同一操作创建新的子上下文。随着我们的扩展,当应用程序移动到后台时,这可能会导致用户丢失他们的更改,无法在对象之间建立关系,因为它们位于不同的上下文中。

为了解决这个问题,我们可以将嵌套上下文以及该上下文中新创建的对象存储在专用结构中,如下所示:

struct CreateOperation<Object: NSManagedObject>: Identifiable {
    let id = UUID()
    let childContext: NSManagedObjectContext
    let object: Object
    
    init(withParentContext parentContext: NSManagedObjectContext) {
        childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
        childContext.parent = parentContext
        object = Object(context: childContext)
    }
}
Run Code Online (Sandbox Code Playgroud)

现在要创建一个新对象并呈现随附的 UI 进行编辑,我们只需创建该结构的一个实例。

struct ItemList: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(sortDescriptors: [SortDescriptor(\.name)])
    private var items: FetchedResults<Item>
    
    @State private var configuration: CreateOperation<Item>?
    
    var body: some View {
        NavigationView {
            List {
                ForEach(items) { item in
                    Text(item.name ?? "")
                }
            }
            
            .toolbar {
                Button(action: addItem) {
                    Label("Add Item", systemImage: "plus")
                }
            }
        }
        .sheet(item: $configuration) { configuration in
            ItemEditor(item: configuration.object)
                .environment(\.managedObjectContext, configuration.childContext)
        }
    }
    
    private func addItem() {
        configuration = CreateOperation(withParentContext: viewContext)
    }
}
Run Code Online (Sandbox Code Playgroud)
struct ItemEditor: View {
    @Environment(\.dismiss) private var dismiss
    @Environment(\.managedObjectContext) private var context
    
    @ObservedObject var item: Item
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: Binding($item.name)!)
            }
            .navigationTitle("New Item")
            .toolbar {
                Button("Save") {
                    try? context.save()
                    dismiss()
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)