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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Drag and Drop Tutorial for macOS
30 mins
- Getting Started
- Meet the Project App
- Pasteboards and Dragging Sessions
- Creating a Dragging Destination
- Handling an Exit
- Tell the User What’s Happening
- Wrap up the Drag
- Use DestinationView’s Data
- Creating a Dragging Source
- Supplying a Standard Dragging Type
- Start a Dragging Session
- Take the TIFF
- Unarchive the Image Data
- Show me the Image Data!
- Dragging Custom Types
- Create the Dragging Source
- Accept the New Type
- Handle the Action Instruction
- Where to go From Here?
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.
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:
- Reset
isReceivingDrag
flag tofalse
. - Convert the window-based coordinate to a view-relative coordinate.
- Hand off any image URLs to the delegate for processing, and return
true
— else you reject the drag operation returningfalse
.
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:
- It hides the Drag Images Here label.
- It figures out the maximum size for the dropped image while holding the aspect ratio constant.
- It constructs a subview with that size, centers it on the drop point and adds it to the view hierarchy.
- 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:
- Creating an image with the contents from the URLs.
- If there is more than one image, this offsets the images’ centers a bit to create a layered, randomized effect.
- 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!
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.
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.