又过了几个月,我再次遇到了这个问题。
自 9 月 18 日发表最后一条评论以来,我一直致力于解决使用 Core Data 构建基于 SwiftUI 文档的应用程序的难题。
通过更深入的研究,我了解到UIManagedDocument(分别是其父级UIDocument)基础架构与 SwiftUI 尝试实现的架构非常接近/相似。 SwiftUI 甚至在后台使用UIDocument 来发挥“它的魔力”。 UIDocument 和 UIManagedDocument 只是 Objective-C 是主要语言时代的一些更古老的残余。没有 Swift,也没有值类型。
一般来说,我可以在UIManagedDocument 中为您提供以下提示,以解决您在使用 Core Data 时遇到的挑战:
- 首先,如果您想使用Core Data,您将不得不使用基于包/捆绑包的文档格式。这意味着您的
UTType 必须符合.package(=com.apple.package 在您的 Info.plist 文件中)。您将无法让 Core Data 仅使用纯文件文档。
extension UTType {
static var exampleDocument: UTType {
UTType(exportedAs: "com.example.mydocument", conformingTo: .package)
}
}
-
使用基于ReferenceFileDocument 的类为您的UIManagedDocument 构建包装文档。这是必要的,因为您必须知道释放对象的时间。在deinit 中,您必须调用managedDocument.close() 以确保UIManagedDocument 正确关闭并且不会丢失数据。
-
打开现有文档时将调用所需的函数init(configuration:)。遗憾的是,在使用UIManagedDocument 时它没有用,因为我们只能访问文档的FileWrapper 而不能访问URL。但是URL 是初始化UIManagedDocument 所需要的。
-
所需的函数snapshot(contentType:) 和fileWrappper(snapshot:, configuration:) 仅用于创建一个新的空文档。 (这是因为我们不会使用集成了UndoManager 的SwiftUI,而是使用来自UIManagedDocument 和Core Datas NSManagedObjectContext 的那个。)因此与Snapshot 的类型无关。您可以使用Date 或Int,因为使用第一个函数拍摄的快照不是您要在第二个函数中写入的内容。
-
fileWrappper(snapshot:, configuration:) 函数应该返回一个空的UIManagedDocument 的文件结构。这意味着,它应该包含一个目录StoreContent 和一个带有持久存储文件名的空文件(默认为persistentStore),如下面的屏幕截图所示。
persistentStore-shm 和 persistentStore-wal 文件将在 Core Data 启动时自动创建,因此我们不必提前创建它们。
我正在使用以下表达式创建代表文档的FileWrapper:(MyManagedDocument 是我的UIManagedDocument 子类)
FileWrapper(directoryWithFileWrappers: [
"StoreContent" : FileWrapper(directoryWithFileWrappers: [
MyManagedDocument.persistentStoreName : FileWrapper(regularFileWithContents: Data())
])
])
- 以上步骤允许我们创建一个空文档。但它仍然无法连接到我们的
UIManagedDocument 子类,因为我们不知道文档(由我们创建的FileWrapper 表示)的位置。幸运的是,SwiftUI 将ReferenceFileDocumentConfiguration 中当前打开的文档的URL 传递给我们,可以在DocumentGroup 内容闭包中访问。然后可以使用属性fileURL 从包装器中最终创建和打开我们的UIManagedDocument 实例。我这样做如下:(file.document 是我们ReferenceFileDocument 类的一个实例)
DocumentGroup(newDocument: { DemoDocument() }) { file in
ContentView()
.onAppear {
if let url = file.fileURL {
file.document.open(fileURL: url)
}
}
}
struct ContentView: View {
@EnvironmentObject var document: DemoDocument
var body: some View {
if let managedDocument = document.managedDocument {
DocumentView()
.environment(\.managedObjectContext, managedDocument.managedObjectContext)
} else {
ProgressView("Loading")
}
}
}
但是你很快就会遇到一些问题/崩溃:
-
打开文档时,您会看到应用程序冻结。似乎 UIManagedDocument open 或 close 不会完成/返回。
这是由于一些僵局。 (你可能还记得,我最初告诉你 SwiftUI 在幕后使用UIDocument?这可能是导致死锁的原因:我们已经在运行一些open,而我们尝试执行另一个open 命令。解决方法:在后台队列上运行对open 和close 的所有调用。
-
当您在之前关闭一个文档后尝试打开另一个文档时,您的应用会崩溃。您可能会看到以下错误:
warning: Multiple NSEntityDescriptions claim the NSManagedObject subclass 'Item' so +entity is unable to disambiguate.
warning: 'Item' (0x6000023c4420) from NSManagedObjectModel (0x600003781220) claims 'Item'.
warning: 'Item' (0x6000023ecb00) from NSManagedObjectModel (0x600003787930) claims 'Item'.
error: +[Item entity] Failed to find a unique match for an NSEntityDescription to a managed object subclass
在调试了几个小时后,我了解到NSManagedObjectModel(由我们的UIManagedDocument 实例化)在关闭文档和打开另一个文档之间没有释放。 (有充分的理由,这不是必需的,因为我们无论如何都会对我们打开的下一个文件使用相同的模型)。我发现这个问题的解决方案是覆盖我的UIManagedDocument 子类的managedObjectModel 变量并返回我从我的应用程序包中“手动”加载的NSManagedObjectModel。我想有更好的方法可以做到这一点,但这是我正在使用的代码:
class MyManagedDocument: UIManagedDocument {
// We fetch the ManagedObjectModel only once and cache it statically
private static let managedObjectModel: NSManagedObjectModel = {
guard let url = Bundle(for: MyManagedDocument.self).url(forResource: "Model", withExtension: "momd") else {
fatalError("Model.xcdatamodeld not found in bundle")
}
guard let mom = NSManagedObjectModel(contentsOf: url) else {
fatalError("Model.xcdatamodeld not load from bundle")
}
return mom
}()
// Make sure to use always the same instance of the model, otherwise we get crashes when opening another document
override var managedObjectModel: NSManagedObjectModel {
Self.managedObjectModel
}
}
所以这个答案变得非常冗长,但我希望它对其他在这个话题上苦苦挣扎的人有所帮助。我已经将this gist 与我的工作示例放在一起进行复制和探索。