0

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.

slashlos
  • 913
  • 9
  • 17

2 Answers2

0

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.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • 1
    If the term "asynchronous" is strange to you, you might want to read http://www.programmingios.net/returning-a-value-from-asynchronous-code/ – matt Mar 17 '20 at 16:55
  • Yes, exactly but then the referenced answer presumed correct, I cannot otherwise see why an extension would behave differently? One plausible solution I'm thinking would to pass the saveAs() a completion handler but I'm confused if the other answer works? I in-lined my extension and yeah same behavior so perhaps the completion() is the only solution? But still confused between these two. – slashlos Mar 17 '20 at 19:53
  • 1
    I can't help you with some _other_ question. I believe what I'm saying to be correct about _your_ code, and it should be easy to confirm it just by lacing your code heavily with more `print` statements. – matt Mar 17 '20 at 20:47
  • Yes you right of course, my original question and its context was re: its use as an extension vs the other answer as it was in-line; yet both exhibit the same behavior described. In olden days the sheet completion was better understood I think. – slashlos Mar 17 '20 at 21:59
0

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 ;-)

slashlos
  • 913
  • 9
  • 17