1

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.

Bob Peterson
  • 636
  • 7
  • 16

1 Answers1

1

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"))
Casey
  • 6,531
  • 24
  • 43
  • Removing the completion block does not cause a crash. More info: `MyDocument.contents:forType:` does get called, so I know the save was initiated. It is as if something in UIDocument is being forced to run on the main thread/queue despite my explicitly initiating it on a new dispatch queue—which seems too strange to contemplate. – Bob Peterson Apr 16 '19 at 00:16
  • I think you misunderstood the recommendation, try putting the save code on the main thread instead of behind the `self.saveQueue.async`. then the code should crash. better yet, leave your code as is and add the symbolic breakpoint for `objc_exception_throw` – Casey Apr 16 '19 at 00:19
  • I see. That was the first thing I did. But: "A typical document-based application calls open(completionHandler:), close(completionHandler:), and save(to:for:completionHandler:) on the main thread. When the read or save operation kicked off by these methods concludes, the completion-handler block is executed on the same dispatch queue on which the method was invoked, allowing you to complete any tasks contingent on the read or save operation. If the operation is not successful, false is passed into the completion-hander block." [https://developer.apple.com/documentation/uikit/uidocument] – Bob Peterson Apr 16 '19 at 00:21
  • No crash even when I remove `saveQueue.async`, and no crash when I move `saveQueue.async` inside the save completion block. The completion block is not being called when the dispatch queue is used. When I remove uses of the dispatch queue and semaphore, the completion block executes after `activityViewController:itemForActivityType` returns. – Bob Peterson Apr 16 '19 at 00:27
  • hmm, ok. i'll keep digging in then and see if i can get it to save properly! i have the same thing happening, the completion block never fires when behind the async – Casey Apr 16 '19 at 00:29
  • The issue seems to be that `UIDocument.save` must be overridden in the subclass in order for sharing like this. How odd, as it worked in other situations. Thanks so much Casey. – Bob Peterson Apr 17 '19 at 12:48