Drag and Drop Tutorial for macOS

The drag-and-drop mechanism has always been an integral part of Macs. Learn how to adopt it in your apps with this drag and drop tutorial for macOS. By Warren Burton.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Supplying a Standard Dragging Type

The dragging source will be ImageSourceView — the class of the view that has the unicorn. Your objective is simple: get that unicorn onto your collage.

The class needs to adopt the necessary protocols NSDraggingSource and NSPasteboardItemDataProvider, so open ImageSourceView.swift and add the following extensions:

// MARK: - NSDraggingSource
extension ImageSourceView: NSDraggingSource {
  //1.
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
    return .generic
  }
}

// MARK: - NSDraggingSource
extension ImageSourceView: NSPasteboardItemDataProvider {
  //2.
  func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: String) {
    //TODO: Return image data
  }
}
  1. This method is required by NSDraggingSource. It tells the dragging session what sort of operation you’re attempting when the user drags from the view. In this case it’s a generic operation.
  2. This implements the mandatory NSPasteboardItemDataProvider method. More on this soon — for now it’s just a stub.

Start a Dragging Session

In a real world project, the best moment to initiate a dragging session depends on your UI.

With the project app, this particular view you’re working in exists for the sole purpose of dragging, so you’ll start the drag on mouseDown(with:).

In other cases, it may be appropriate to start in the mouseDragged(with:) event.

Add this method inside the ImageSourceView class implementation:

override func mouseDown(with theEvent: NSEvent) {
  //1.
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setDataProvider(self, forTypes: [kUTTypeTIFF])
  
  //2.
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
  
  //3.
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
}

Things get rolling when the system calls mouseDown(with:) when you click on a view. The base implementation does nothing, eliminating the need to call super. The code in the implementation does all of this:

  1. Creates an NSPasteboardItem and sets this class as its data provider. A NSPasteboardItem is the box that carries the info about the item being dragged. The NSPasteboardItemDataProvider provides data upon request. In this case you’ll supply TIFF data, which is the standard way to carry images around in Cocoa.
  2. Creates a NSDraggingItem and assigns the pasteboard item to it. A dragging item exists to provide the drag image and carry one pasteboard item, but you don’t keep a reference to the item because of its limited lifespan. If needed, the dragging session will recreate this object. snapshot() is one of the helper methods mentioned earlier. It creates an NSImage of an NSView.
  3. Starts the dragging session. Here you trigger the dragging image to start following your mouse until you drop it.

Build and run. Try to drag the unicorn onto the top view.

buildrun-drag-unicorn

An image of the view follows your mouse, but it slides back on mouse up because DestinationView doesn’t accept TIFF data.

Take the TIFF

In order to accept this data, you need to:

  1. Update the registered types in setup() to accept TIFF data
  2. Update shouldAllowDrag() to accept the TIFF type
  3. Update performDragOperation(_:) to take the image data from the pasteboard

Open DestinationView.swift.

Replace the following line:

 
var acceptableTypes: Set<String> { return [NSURLPboardType] }

With this:

 
var nonURLTypes: Set<String>  { return [String(kUTTypeTIFF)] }
var acceptableTypes: Set<String> { return nonURLTypes.union([NSURLPboardType]) }

You’ve just registered the TIFF type like you did for URLs and created a subset to use next.

Next, go to shouldAllowDrag(:_), and add find the return canAccept method. Enter the following just above the return statement:

 
else if let types = pasteBoard.types, nonURLTypes.intersection(types).count > 0 {
  canAccept = true
}

Here you’re checking if the nonURLTypes set contains any of the types received from the pasteboard, and if that’s the case, accepts the drag operation. Since you added a TIFF type to that set, the view accepts TIFF data from the pasteboard.

Unarchive the Image Data

Lastly, update performDragOperation(_:) to unarchive the image data from the pasteboard. This bit is really easy.

Cocoa wants you to use pasteboards and provides an NSImage initializer that takes NSPasteboard as a parameter. You’ll find more of these convenience methods in Cocoa when you start exploring drag and drop more.

Locate performDragOperation(_:), and add the following code at the end, just above the return sentence return false:

else if let image = NSImage(pasteboard: pasteBoard) {
  delegate?.processImage(image, center: point)
  return true
}

This extracts an image from the pasteboard and passes it to the delegate for processing.

Build and run, and then drag that unicorn onto the sticker view.

buildrun-drag-unicorn-plus

You’ll notice that now you get a green + on your cursor.

The destination view accepts the image data, but the image still slides back when you drop. Hmmm. What’s missing here?

Show me the Image Data!

You need to get the dragging source to supply the image data — in other words: fulfil its promise.

Open ImageSourceView.swift and replace the contents of pasteboard(_:item:provideDataForType:) with this:

//1.
if let pasteboard = pasteboard, type == String(kUTTypeTIFF), let image = NSImage(named:"unicorn") {
  //2.
  let finalImage = image.tintedImageWithColor(NSColor.randomColor())
  //3.
  let tiffdata = finalImage.tiffRepresentation
  pasteboard.setData(tiffdata, forType:type)
}

In this method, the following things are happening:

  1. If the desired data type is kUTTypeTIFF, you load an image named unicorn.
  2. Use one of the supplied helpers to tint the image with a random color. After all, colorful unicorns are more festive than a smattering of all-black unicorns. :]
  3. Transform the image into TIFF data and place it on the pasteboard.

Build and run, and drag the unicorn onto the sticker view. It’ll drop and place a colored unicorn on the view. Great!

buildrun-add-unicorns

So.many.unicorns!

Dragging Custom Types

Unicorns are pretty fabulous, but what good are they without magical sparkles? Strangely, there’s no standard Cocoa data type for sparkles. I bet you know what comes next. :]

sparkle

Note: In the last section you supplied a standard data type. You can explore the types for standard data in the API reference.

In this section you’ll invent your own data type.

These are the tasks on your to-do list:

  1. Create a new dragging source with your custom type.
  2. Update the dragging destination to recognize that type.
  3. Update the view controller to react to that type.

Create the Dragging Source

Open AppActionSourceView.swift. It’s mostly empty except for this important definition:

enum SparkleDrag {
  static let type = "com.razeware.StickerDrag.AppAction"
  static let action = "make sparkles"
}

This defines your custom dragging type and action identifier.

Dragging source types must be Uniform Type Identifiers. These are reverse-coded name paths that describe a data type.

For example, if you print out the value of kUTTypeTIFF you’ll see that it is the string public.tiff.

To avoid a collision with an existing type, you can define the identifier like this: bundle identifier + AppAction. It is an arbitrary value, but you keep it under the private namespace of the application to minimize the risk of using an existing name.

If you attempt to construct a NSPasteboardItem with a type that isn’t UTI, the operation will fail.

Now you need to make AppActionSourceView adopt NSDraggingSource. Open AppActionSourceView.swift and add the following extension:

// MARK: - NSDraggingSource
extension AppActionSourceView: NSDraggingSource {
  
  func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor
    context: NSDraggingContext) -> NSDragOperation {
    
    switch(context) {
    case .outsideApplication:
      return NSDragOperation()
    case .withinApplication:
      return .generic
    }
  }
}

This code block differs from ImageSourceView because you’ll place private data on the pasteboard that has no meaning outside the app. That’s why you’re using the context parameter to return a NSDragOperation() when the mouse is dragged outside your application.

You’re already familiar with the next step. You need to override the mouseDown(with:) event to start a dragging session with a pasteboard item.

Add the following code into the AppActionSourceView class implementation:

override func mouseDown(with theEvent: NSEvent) {
  
  let pasteboardItem = NSPasteboardItem()
  pasteboardItem.setString(SparkleDrag.action, forType: SparkleDrag.type)
  let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
  draggingItem.setDraggingFrame(self.bounds, contents:snapshot())
  
  beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
  
}

What did you do in there?

You constructed a pasteboard item and placed the data directly inside it for your custom type. In this case, the data is a custom action identifier that the receiving view may use to make a decision.

You can see how this differs from ImageSourceView in one way. Instead of deferring data generation to the point when the view accepts the drop with the NSPasteboardItemDataProvider protocol, the dragged data goes directly to the pasteboard.

Why would you use the NSPasteboardItemDataProvider protocol? Because you want things to move as fast as possible when you start the drag session in mouseDown(with:).

If the data you’re moving takes too long to construct on the pasteboard, it’ll jam up the main thread and frustrate users with a perceptible delay when they start dragging.

In this case, you place a small string on the pasteboard so that it can do it right away.