Darren Ford
Darren Ford

Reputation: 896

Drag a file promise from a NSView onto the desktop or another application (macOS)

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

Answers (1)

Darren Ford
Darren Ford

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.

Basic overview

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)

Starting the drag

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.

  1. Tell the pasteboard that we will provide the file type UTI for the drop (kPasteboardTypeFilePromiseContent)
  2. Tell the pasteboard that we will provide a file promise (kPasteboardTypeFileURLPromise) for the data type specified in (1)

Responding to the receiver asking for the content that we'll provide

kPasteboardTypeFilePromiseContent

This is the first callback from the receiver of the drop (via pasteboard(pasteboard:item:provideDataForType:))

  1. The receiver is asking us what type (UTI) of file we will provide.
  2. Respond by setting the UTI (using setString("") on the pasteboard object) for the type kPasteboardTypeFilePromiseContent

Responding to the receiver asking for the file

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.

  1. The receiver tells us the folder to write our content to (com.apple.pastelocation)
  2. Write the data to disk inside the folder that the receiver has told us.
  3. Respond by setting the resulting URL of the written file (using setString() on the pasteboard object) for the type kPasteboardTypeFileURLPromise. Note that the format of this string needs to be file:///... so .absoluteString() needs to be used.

And we're done!

Sample


// 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.

https://developer.apple.com/documentation/appkit/documents_files_and_icloud/supporting_drag_and_drop_through_file_promises

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

Related Questions