SwiftUI drag & drop does not support file promises

SwiftUI doesn’t offer anything equivalent to NSFilePromiseProvider, i.e. to write data to the drop destination. You have to ditch SwiftUI and use AppKit’s drag & drop APIs instead.

FB13583826.

Is that it?

I know that’s not a very helpful in some sense, but I wasted days trying to figure out how to implement this very basic drag and drop functionality, in SwiftUI. From what I found online, I’m not the only person who’s struggled with this. So hopefully this post saves others from long and frustrating searches.

A big part of the difficulty in answering this simple question – does SwiftUI support this or not – is that a lot of documentation (first & third party) around SwiftUI’s APIs uses the word “promise” but not in the same way. They merely mean that the pasteboard data is populated on first access, not that it actually follows the file promise protocol.

Best alternative (other than ditching SwiftUI)

The closest you can get is to write data into some arbitrary location that you have to choose blindly (not having any idea where the actual destination is), and then rely on the receiving app to move or copy the file from there. It doesn’t matter, in this regard, whether you use the Transferable-based APIs or the NSItemProvider-based APIs.

view.onDrag {
    let provider = NSItemProvider()
    
    provider.registerDataRepresentation(for: UTType.fileURL) {
        do {
            let tmpDir = try FileManager.default.url(for: .itemReplacementDirectory,
                                                     in: .userDomainMask,
                                                     appropriateFor: URL.temporaryDirectory,
                                                     create: true)

            // You'll need to provide the `suggestedFileName` based on the particulars of your use-case.
            let file = tmpDir.appending(component: suggestedFileName)

            try bytes.write(to: file, options: .withoutOverwriting)

            $0(file.dataRepresentation, nil)
        } catch {
            $0(nil, error)
        }

        return nil
    }
    
    return provider
}

This makes it impossible to avoid unnecessary file copies, and requires you to keep the file around permanently, since you have no idea when the receiver is done with it.

e.g. if the Finder is the drop destination, you have no idea what volume the promised file was dropped on, so you don’t know which volume to create it on. If you guess wrong, the Finder is forced to copy the file. Not only does this confuse the user (by showing a copy badge on the mouse pointer) but it wastes time (in performing the copy) and the original file is left in place, requiring manual clean-up (which is impossible to do correctly because you have no idea when the receiving application is done with the temporary file – and the receiver might hold a reference to it permanently).

Understanding the SwiftUI drag & drop APIs

Tangentially, I found most documentation on SwiftUI’s drag & drop APIs – especially Apple’s – to be very poorly written, which is particularly frustrating in this case because the APIs are not intuitive in the slightest.

And most code / usage examples focus exclusively on trivial, disinteresting cases (e.g. dragging images around within a single window).

Only after I’d spend days reverse-engineering the APIs to figure out how they actually work and how they’re supposed to be used, did I stumble upon Dave Rahardja‘s All about Item Providers. It’s by far the best documentation I’ve found on drag & drop in SwiftUI. Note though that Dave also uses the word “promise” a lot even though he’s never actually talking about actual file promises.

Also, I found that the newer and ostensibly better Transferable-based API was harder to understand and harder to use than the NSItemProvider-based APIs. I recommend going straight to, and sticking with, the latter. Your code will be simpler and more reliable.

Leave a Comment