Reputation: 896
I need to be able to drag a file representation (a pdf in my case) from an NSView contained within my application onto the Desktop or another application that supports opening PDF files.
Upvotes: 2
Views: 1199
Reputation: 896
I spent a few hours trying to get this working in my own app, and I thought I'd add my solution here as there's a lot of half-solutions online, some of which rely on Obj-C extensions and others which are outdated and are no longer supported. I'm hoping this post os the sort of post I'd wished I'd found during my own searches. I'm also aware of all of the minutae of the system (for example, using file coordinators instead of a direct write) but this seems to be the minimum code required to implement.
I've also provided a simple Swift NSView implementation.
The operation occurs in three main stages.
You'll need to make your view (or other control) a 'Data Provider' for the drag by implementing the NSPasteboardItemDataProvider
protocol. The majority of the work required (other than starting the drag) occurs in the following protocol function.
func pasteboard(_ pasteboard: NSPasteboard?, item _: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType)
This section occurs when the drag starts. In my case, I was doing this in mouseDown(), but you could also do this in the mouseDragged for example.
kPasteboardTypeFilePromiseContent
)kPasteboardTypeFileURLPromise
) for the data type specified in (1)kPasteboardTypeFilePromiseContent
This is the first callback from the receiver of the drop (via pasteboard(pasteboard:item:provideDataForType:)
)
kPasteboardTypeFilePromiseContent
kPasteboardTypeFileURLPromise
This is the second callback from the receiver (via pasteboard(pasteboard:item:provideDataForType:)
)
The receiver is asking us to write the data to a file on disk.
com.apple.pastelocation
)kPasteboardTypeFileURLPromise
. Note that the format of this string needs to be file:///...
so .absoluteString()
needs to be used.And we're done!
// Some definitions to help reduce the verbosity of our code
let PasteboardFileURLPromise = NSPasteboard.PasteboardType(rawValue: kPasteboardTypeFileURLPromise)
let PasteboardFilePromiseContent = NSPasteboard.PasteboardType(rawValue: kPasteboardTypeFilePromiseContent)
let PasteboardFilePasteLocation = NSPasteboard.PasteboardType(rawValue: "com.apple.pastelocation")
class MyView: NSView {
override func mouseDown(with event: NSEvent) {
let pasteboardItem = NSPasteboardItem()
// (1, 2) Tell the pasteboard item that we will provide both file and content promises
pasteboardItem.setDataProvider(self, forTypes: [PasteboardFileURLPromise, PasteboardFilePromiseContent])
// Create the dragging item for the drag operation
let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
draggingItem.setDraggingFrame(self.bounds, contents: image())
// Start the dragging session
beginDraggingSession(with: [draggingItem], event: event, source: self)
}
}
Then, in your Pasteboard Item Data provider extension...
extension MyView: NSPasteboardItemDataProvider {
func pasteboard(_ pasteboard: NSPasteboard?, item _: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {
if type == PasteboardFilePromiseContent {
// The receiver will send this asking for the content type for the drop, to figure out
// whether it wants to/is able to accept the file type (3).
// In my case, I want to be able to drop a file containing PDF from my app onto
// the desktop or another app, so, add the UTI for the pdf (4).
pasteboard?.setString("com.adobe.pdf", forType: PasteboardFilePromiseContent)
}
else if type == PasteboardFileURLPromise {
// The receiver is interested in our data, and is happy with the format that we told it
// about during the kPasteboardTypeFilePromiseContent request.
// The receiver has passed us a URL where we are to write our data to (5).
// It is now waiting for us to respond with a kPasteboardTypeFileURLPromise
guard let str = pasteboard?.string(forType: PasteboardFilePasteLocation),
let destinationFolderURL = URL(string: str) else {
// ERROR:- Receiver didn't tell us where to put the file?
return
}
// Here, we build the file destination using the receivers destination URL
// NOTE: - you need to manage duplicate filenames yourself!
let destinationFileURL = destinationFolderURL.appendingPathComponent("dropped_file.pdf")
// Write your data to the destination file (6). Do better error handling here!
let pdfData = self.dataWithPDF(inside: self.bounds)
try? pdfData.write(to: destinationFileURL, options: .atomicWrite)
// And finally, tell the receiver where we wrote our file (7)
pasteboard?.setString(destinationFileURL.absoluteString, forType: PasteboardFileURLPromise)
}
}
If anyone finds issues with this or it's completely incorrect please let me know! It seems to work for my app at least.
As Willeke has pointed out, Apple has some sample code for using the (newer) NSFilePromiseProvider mechanism for drag drop.
I wish my search had started at Apple's Developer pages instead of Google 🙃. Oh well! The sample provided is valid and still works, so if this post helps someone locate more cohesive info regarding drag drop then fantastic.
Upvotes: 4