swiftconcurrencynsdocument

`NSDocument`'s `data(ofType:)` getting data from (async) `actor`


I've a document based macOS, that's using a NSDocument based subclass.

For writing the document's file I need to implement data(ofType:) -> Data which should return the document's data to be stored on disk. This is (of course) a synchronous function.

My data model is an actor with a function that returns a Data representation.

The problem is now that I need to await this function, but data(ofType:) wants the data synchronously.

How can I force-wait (block the main thread) until the actor has done its work and get the data?

EDIT: In light of Sweepers remark that this might be an XY-problem I tried making the model a @MainActor, so the document can access the properties directly. This however doesn't allow me to create the model in the first place:

@MainActor class Model {}

class Document: NSDocument {
let model = Model() <- 'Call to main actor-isolated initializer 'init()' in a synchronous nonisolated context'
}

I then tried to make the whole Document a @MainActor, but that makes my whole app to collapse in compiler errors. Even the simplest of calls need to be performed async. This doesn't allow any kind of upgrade path to the new concurrency system.

In the past my model was protected by a serial background queue and I could basically do queue.sync {} to get the needed data out safely (temporarily blocking the main queue).

I've looked into the saveToURL:ofType:forSaveOperation:completionHandler: and I think I can use this very much to my need. It allows async messaging that saving is finished, so I now override this method and in an async Task fetch the data from the model and store it in temporarily. I then call super, which finally calls data(forType:) where I return the data.


Solution

  • Based on the idea by @Willeke in the comments, I came up with the following solution:

    private var snapshot: Model.Snapshot?
    
    override func save(to url: URL, ofType typeName: String, for saveOperation: NSDocument.SaveOperationType, completionHandler: @escaping (Error?) -> Void) {
        //Get the data and continue later
        Task {
            snapshot = await model.getSnapshot()
            super.save(to: url, ofType: typeName, for: saveOperation, completionHandler: completionHandler)
        }
    }
    
    override func data(ofType typeName: String) throws -> Data {
        defer { snapshot = nil }
    
        guard let snapshot = snapshot else {
            throw SomeError()
        }
    
        let encoder = JSONEncoder()
        let data = try encoder.encode(snapshot)
    
        return data
    }
    
    

    As the save() function is prepared to handle the save result asynchronous we first take the snapshot of the data and then let the save function continue.