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 2 of 4 of this article. Click here to view the first page.

Handling an Exit

What enters the view may also exit, so the app needs to react when a dragging session has exited your view without a drop. Add the following code:

override func draggingExited(_ sender: NSDraggingInfo?) {
  isReceivingDrag = false
}

You’ve overridden draggingExited(_:) and set the isReceivingDrag variable to false.

Tell the User What’s Happening

You’re almost done with the first stretch of coding! Users love to see a visual cue when something is happening in the background, so the next thing you’ll add is a little drawing code to keep your user in the loop.

Still in DestinationView.swift, find draw(:_) and replace it with this.

override func draw(_ dirtyRect: NSRect) {
  
  if isReceivingDrag {
    NSColor.selectedControlColor.set()
    
    let path = NSBezierPath(rect:bounds)
    path.lineWidth = Appearance.lineWidth
    path.stroke()
  }
}

This code draws a system-colored border when a valid drag enters the view. Aside from looking sharp, it makes your app consistent with the rest of the system by providing a visual when it accepts a dragged item.

Note: Want to know more about custom drawing? Check out our Core Graphics on macOS Tutorial.

Note: Want to know more about custom drawing? Check out our Core Graphics on macOS Tutorial.

Build and run then try dragging an image file from Finder to StickerDrag. If you don’t have an image handy, use sample.jpg inside the project folder.

buildrun-add-plus

You can see that the cursor picks up a + symbol when inside the view and that the view draws a border around it.

When you exit the view, the border and + disappears; absolutely nothing happens when you drag anything but an image file.

Wrap up the Drag

Now, on to the final step for this section: You have to accept the drag, process the data and let the dragging session know that this has occurred.

Append the DestinationView class implementation with the following:

override func prepareForDragOperation(_ sender: NSDraggingInfo) -> Bool {
  let allow = shouldAllowDrag(sender)
  return allow
}

The system calls the above method when you release the mouse inside the view; it’s the last chance to reject or accept the drag. Returning false will reject it, causing the drag image to slide back to its origination. Returning true means the view accepts the image. When accepted, the system removes the drag image and invokes the next method in the protocol sequence: performDragOperation(_:).

Add this method to DestinationView:

override func performDragOperation(_ draggingInfo: NSDraggingInfo) -> Bool {
  
  //1.
  isReceivingDrag = false
  let pasteBoard = draggingInfo.draggingPasteboard()
  
  //2.
  let point = convert(draggingInfo.draggingLocation(), from: nil)
  //3.
  if let urls = pasteBoard.readObjects(forClasses: [NSURL.self], options:filteringOptions) as? [URL], urls.count > 0 {
    delegate?.processImageURLs(urls, center: point)
    return true
  }
  return false
  
}

Here’s what you’re doing in there:

  1. Reset isReceivingDrag flag to false.
  2. Convert the window-based coordinate to a view-relative coordinate.
  3. Hand off any image URLs to the delegate for processing, and return true — else you reject the drag operation returning false.

Note: Feeling extra heroic? If you were to make an animated drop sequence, performDragOperation(:_) would be the best place to start the animation.

Note: Feeling extra heroic? If you were to make an animated drop sequence, performDragOperation(:_) would be the best place to start the animation.

Congratulations! You’ve just finished the first section and have done all the work DestinationView needs to receive a drag.

Use DestinationView’s Data

Next up you’ll use the data that DestinationView provides in its delegate.

Open StickerBoardViewController.swift and introduce yourself to the class that is the delegate of DestinationView.

To use it properly, you need to implement the DestinationViewDelegate method that places the images on the target layer. Find processImage(_:center:) and replace it with this.

func processImage(_ image: NSImage, center: NSPoint) {
  
  //1.
  invitationLabel.isHidden = true
  
  //2.
  let constrainedSize = image.aspectFitSizeForMaxDimension(Appearance.maxStickerDimension)
  
  //3.
  let subview = NSImageView(frame:NSRect(x: center.x - constrainedSize.width/2, y: center.y - constrainedSize.height/2, width: constrainedSize.width, height: constrainedSize.height))
  subview.image = image
  targetLayer.addSubview(subview)
  
  //4.
  let maxrotation = CGFloat(arc4random_uniform(Appearance.maxRotation)) - Appearance.rotationOffset
  subview.frameCenterRotation = maxrotation
  
}

This code does the following tricks:

  1. It hides the Drag Images Here label.
  2. It figures out the maximum size for the dropped image while holding the aspect ratio constant.
  3. It constructs a subview with that size, centers it on the drop point and adds it to the view hierarchy.
  4. It randomly rotates the view a little bit for a bit of funkiness.

With all that in place, you’re ready to implement the method so it deals with the image URLs that get dragged into the view.
Replace processImageURLs(_:center:) method with this:

func processImageURLs(_ urls: [URL], center: NSPoint) {
  for (index,url) in urls.enumerated() {
    
    //1.
    if let image = NSImage(contentsOf:url) {
      
      var newCenter = center
      //2.
      if index > 0 {
        newCenter = center.addRandomNoise(Appearance.randomNoise)
      }
      
      //3.
      processImage(image, center:newCenter)
    }
  }
}

What you’re doing here is:

  1. Creating an image with the contents from the URLs.
  2. If there is more than one image, this offsets the images’ centers a bit to create a layered, randomized effect.
  3. Pass the image and center point to the previous method so it can add the image to the view.

Now build and run then drag an image file (or several) to the app window. Drop it!

window-demo-1

Look at that board of images just waiting to be made fearlessly fanciful.

You’re at about the halfway point and have already explored how to make any view a dragging destination and how to compel it to accept a standard dragging type — in this case, an image URL.

Intermission: let's all go to the lobby and get ourselves some drinks. And snacks. And new iMacs

Creating a Dragging Source

You’ve played around with the receiving end, but how about the giving end?

In this section, you’ll learn how to supercharge your app with the ability to be the source by letting those unicorns and sparkles break free and bring glee to the users’ images in the right circumstances.

All dragging sources must conform to the NSDraggingSource protocol. This MVP (most valuable player) takes the task of placing data (or a promise for that data) for one or more types on the dragging pasteboard. It also supplies a dragging image to represent the data.

When the image finally lands on its target, the destination unarchives the data from the pasteboard. Alternatively, the dragging source can fulfil the promise of providing the data.

You’ll need to supply the data of two different types: a standard Cocoa type (an image) and custom type that you create.