URL saveAs extension

69 Views Asked by At

When this URL extension is run as a panel it fails to return a URL, seemingly bypassing the completion:

func saveAs() -> URL? {
    let savePanel = NSSavePanel()
    var saveAsURL : URL? = nil

    savePanel.canCreateDirectories = true
    savePanel.nameFieldStringValue = self.lastPathComponent
    savePanel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!

    if let keyWindow = NSApp.keyWindow {
        savePanel.beginSheetModal(for: keyWindow, completionHandler: { result in
            /*if result == .OK {*/ saveAsURL = savePanel.url //}
        })
    }
    else
    {
        NSApp.activate(ignoringOtherApps: true)

        if savePanel.runModal() == .OK {
            saveAsURL = savePanel.url
        }
    }
    Swift.print("saveAsURL => \(saveAsURL.debugDescription)")

    return saveAsURL
}

but run as standalone window it works fine. There is another related answer here but here the usage is different: i.e.

guard let saveAsURL = URL.init(string: "download.dmg").saveAs() else { return }

where I presume a user cancel would infer the processing should end.

2

There are 2 best solutions below

4
matt On

When you say .runModal your code does a very, very odd thing: it stops and waits for the user to deal with the modal dialog (also known as blocking). That goes back to the extremely early days of OS X / Cocoa, and is an anomaly in behavior. So the result is that by the time we get to the end of the method and your Swift.print and return, we are back from the modal dialog and we have the value from it.

But, as I say, that's totally weird. (In fact, I'm not sure I can think of any other Cocoa call that acts like that.) The .beginSheetModal call behaves normally, i.e. after the call your code goes right on and gets to the end with the print and return before the modal sheet even has a chance to appear. What happens within the modal sheet then happens asynchronously with respect the calling code here — i.e. later. Thus you cannot return any value from the dialog because you'd need a time machine to look into the future. That is the standard pattern for this sort of thing.

0
slashlos On

What I came up with following my suggestion re: a completion handler.

func saveAs(responseHandler: @escaping (URL?) -> Void) {
    let savePanel = NSSavePanel()

    savePanel.canCreateDirectories = true
    savePanel.nameFieldStringValue = self.lastPathComponent
    savePanel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!

    if let keyWindow = NSApp.keyWindow {
        savePanel.beginSheetModal(for: keyWindow, completionHandler: { result in
            responseHandler( result == .OK ? savePanel.url : nil )
         })
    }
    else
    {
        NSApp.activate(ignoringOtherApps: true)

        let result = savePanel.runModal()
        responseHandler( result == .OK ? savePanel.url : nil )
    }
}

and called like this - response delegate; the user clicked a download link on a 'data' (binary) file known by its UTI, so we download then get back to the earlier URL:

    guard url.hasDataContent(), let suggestion = response.suggestedFilename else { decisionHandler(.allow); return }
    let downloadDir = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first!
    let saveURL = downloadDir.appendingPathComponent(suggestion)
    saveURL.saveAs(responseHandler: { saveAsURL in
        if let saveAsURL = saveAsURL {
            self.loadFileAsync(url, to: saveAsURL, completion: { (path, error) in
                if let error = error {
                    NSApp.presentError(error)
                }
                else
                {
                    if appDelegate.isSandboxed() { _ = appDelegate.storeBookmark(url: saveAsURL, options: [.withSecurityScope]) }
                }
            })
        }

        decisionHandler(.cancel)
        self.backPress(self)
     })

I think I like the older way better ;-)