Search code examples
iossemaphoredispatch-asyncuidocument

Create UIDocument on the fly in UIActivityItemProvider


I am using Core Data to store my user's data. I want to offer a Share button in the UI to export the data wherever they wish. I made a class conforming to UIActivityItemSource. It is successful when it simply returns a Data object from activityViewController:itemForActivityType but the file has a system-provided file name. Therefore I'm now trying now to generate a UIDocument on the fly, save it to the file system, and return the URL (UIDocument.fileURL) to the activity view controller. The problem is UIDocument.save is async but I can't return from activityViewController:itemForActivityType until the file is saved. My latest code looks like this:

The class init:

let saveQueue = DispatchQueue(label: "saveQueue")
let saveSemaphore = DispatchSemaphore(value: 0)

...

func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
    var success = false
    self.saveQueue.async {
        self.document.save(to: self.document.fileURL,
                           for: .forOverwriting) {(didSucceed) in
            success = didSucceed
            self.saveSemaphore.signal()
        }
    }
    saveSemaphore.wait()
    return success ? document.fileURL : nil
}

The behavior is the async code starts, but the save's completion function is never called. What have(n't) I done wrong?


My final solution, per Casey's help, was to override UIDocument.save.

override func save(to url: URL, for saveOperation: UIDocument.SaveOperation, completionHandler: ((Bool) -> Void)? = nil) {
    do {
        let contents = try self.contents(forType: GlobalConst.exportType) as! Data
        try contents.write(to: url, options: [.atomicWrite])
        completionHandler?(true)
    } catch {
        print("write error: \(error)")
        completionHandler?(false)
    }
}

I have yet to understand why the superclass save was a problem, but I can let it slide.


Solution

  • In order to get it to work I had to create a subclass of UIDocument and override func save(to:for:completionHandler:)

    class MyDocument: UIDocument {
    
        var body: String = "Hello world, this is example content!"
    
        override func save(to url: URL, for saveOperation: UIDocument.SaveOperation, completionHandler: ((Bool) -> Void)? = nil) {
            do {
                try self.body.write(toFile: url.path, atomically: true, encoding: .utf8)
                completionHandler?(true)
            } catch {
                print("write error: \(error)")
                completionHandler?(false)
            }
        }
    }
    

    Your code stays exactly the same, just make sure self.document is of type MyDocument (or whatever subclass you likely already have).

    let document = MyDocument(fileURL: URL(fileURLWithPath: NSHomeDirectory() + "/Documents/output.txt"))