FileDocument with UIManagedDocument/core data
Asked Answered
R

1

8

When using SwiftUI to create a document based app, the default document type is to subclass FileDocument.

All examples lead to simple value types to be used in this document type.

I'm looking to create a UIManagedDocument in SwiftUI but there doesn't seem to be any mention of using FileDocument with core data. I noticed a ReferenceFileDocument but this leads to no examples either...

Has anyone had any experience of using either SwiftUI document type for core data based documents?

Riyal answered 18/6, 2021 at 18:41 Comment(1)
Same issue here... I have a working ReferenceFileDocument (you have to use/connect the UndoManager to make it save), but don't know how to make it work with UIManagedDocumentNessie
N
12

After some more months, I came across this question once again.
Since my last comment on September 18th, I've worked myself on solving the puzzle of building a SwiftUI document-based app using Core Data.

Looking more in-depth I learned that the UIManagedDocument (respectively its parent UIDocument) infrastructure is really close/similar to what SwiftUI tries to implement. SwiftUI even uses UIDocument in the background to do "its magic". UIDocument and UIManagedDocument are simply some more archaic remnants of times where Objective-C was the dominant language. There was no Swift and there were no value-types.

In general I can give you the following tips to solve your challenge using Core Data within a UIManagedDocument:

  • first of all, if you want to use Core Data, you will have to use a package/bundle based document format. This means your UTType will have to conform to .package (=com.apple.package in your Info.plist file). You won't be able to make Core Data work with only a plain file document.
extension UTType {
    static var exampleDocument: UTType {
        UTType(exportedAs: "com.example.mydocument", conformingTo: .package)
    }
}
  • use a ReferenceFileDocument based class to build a wrapper document for your UIManagedDocument. This is necessary because you will have to know about the time when the object is released. In deinit you will have to call managedDocument.close() to ensure the UIManagedDocument is properly closed and no data is lost.

  • the required function init(configuration:) is going to be called when an existing document is opened. Sadly, it is of no use when working with UIManagedDocument because we have only access to the FileWrapper of the document and not the URL. But the URL is what you need to initialize the UIManagedDocument.

  • the required function snapshot(contentType:) and fileWrappper(snapshot:, configuration:) is only used to create a new empty document. (This is because we won't use the SwiftUI integrated UndoManager but the one from UIManagedDocument respectively Core Datas NSManagedObjectContext.) Therefore it is not relevant what your type for Snapshot is. You can use a Date or Int because the snapshot taken with the first function is not what you are going to write in the second function.

  • The fileWrappper(snapshot:, configuration:) function should return the file structure of an empty UIManagedDocument. This means, it should contain a directory StoreContent and an empty file with the filename of the persistent store (default is persistentStore) as in the screenshot below. Screenshot of package content The persistentStore-shm and persistentStore-wal files are going to be created automatically when Core Data is starting up, so we do not have to create them in advance.
    I am using the following expression to create the FileWrapper representing the document: (MyManagedDocument is my UIManagedDocument subclass)

FileWrapper(directoryWithFileWrappers: [
    "StoreContent" : FileWrapper(directoryWithFileWrappers: [
        MyManagedDocument.persistentStoreName : FileWrapper(regularFileWithContents: Data())
    ])
])
  • above steps allow us to create an empty document. But it still cannot be connected to our UIManagedDocument subclass, because we have no idea where the document (represented by the FileWrapper we have created) is located. Luckily SwiftUI is passing us the URL of the currently opened document in the ReferenceFileDocumentConfiguration which is accessible in the DocumentGroup content closure. The property fileURL can then be used to finally create and open our UIManagedDocument instance from the wrapper. I'm doing this as follows: (file.document is an instance of our ReferenceFileDocument class)
DocumentGroup(newDocument: { DemoDocument() }) { file in
    ContentView()
        .onAppear {
            if let url = file.fileURL {
                file.document.open(fileURL: url)
            }
        }
}
  • in my open(fileURL:) method, I then instantiate the UIManagedDocument subclass and call open to properly initialize it.

  • With above steps you will be able to display your document and access its managedObjectContext in a view similar to this: (DocumentView is a regular SwiftUI view using for example @FetchRequest to query data)

struct ContentView: View {
    @EnvironmentObject var document: DemoDocument

    var body: some View {
        if let managedDocument = document.managedDocument {
            DocumentView()
                .environment(\.managedObjectContext, managedDocument.managedObjectContext)
        } else {
            ProgressView("Loading")
        }
    }
}

But you will soon encounter some issues/crashes:

  1. You see the app freeze when a document is opened. It seems as if UIManagedDocument open or close won't finish/return.
    This is due to some deadlock. (You might remember, that I initially told you that SwiftUI is using UIDocument behind the scene? This is probably the cause of the deadlock: we are running already some open while we try to execute another open command.
    Workaround: run all calls to open and close on a background queue.

  2. Your app crashes when you try to open another document after having previously closed one. You might see errors as:

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

After having debugged this for some hours, I learned that the NSManagedObjectModel (instantiated by our UIManagedDocument) is not released between closing a document and opening another. (Which, by good reason, is not necessary as we would use the same model anyway for the next file we open). The solution I found to this problem was to override the managedObjectModel variable for my UIManagedDocument subclass and return the NSManagedObjectModel which I'm loading "manually" from my apps bundle. I suppose there are nicer ways to do this, but here is the code I'm using:

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
    }
}

So this answer has become really lengthy, but I hope it is helpful to others struggling with this topic. I've put up this gist with my working example to copy and explore.

Nessie answered 15/12, 2021 at 17:37 Comment(3)
You've done huge research! Thank you! I was looking for the answer for quite some time. Unfortunately, this only proves my guess that SwiftUI is simply not yet ready for UIManagedDocument or NSPersistentDocument.Ousley
Thank you. This issue has plagued my UIKit based app as well. My app has a ViewController that allows the user to create new documents, and does not require the user to close the currently opened document. When the document is created I populate it with some default data, and I would get this error. Your solution fixed it. As an aside, my model is stored in a swift package. In that case, be sure to use .module as the Bundle.Procambium
This has helped me massively on the UIManagedDocument side. I'm now trying to implement this in NSPersistentDocument and having an issue. I don't suppose anyone's already tackled this?Riyal

© 2022 - 2024 — McMap. All rights reserved.