March 16, 2018

Swiftly Drag

Posted in Uncategorized at 5:54 pm by tetontech

I was asked for help this week. A person wanted to know how to move User Interface (UI) items around on the screen and be able to drop them on other UI items within the same app. He was writing in Swift for MacOS. I thought it would be easy to find and a good, simple, example. Turns out it wasn’t that easy to find.

There were a lot of examples of dragging files of different types onto or out of an app and dropping them. There were plenty of examples of dragging UI items out of an app to another app. Most of these examples were way over complicated, only had parts of the code shown, or were done with a shallow or poor explanation of what was happening. Yet where was the example this person wanted? I couldn’t find one so I wrote what you see here.

The first thing you need to know to make this example easier to understand is that on MacOS drag-and-drop implementations use the clipboard…the same clipboard used by copy-paste behavior. When you start dragging, you put the UI item being dragged into the clipboard so you can get at it later when you do the drop portion of the code.

Another important point is that this doesn’t need to involve a view controller. The simplest solution doesn’t require a new view controller and doesn’t the existing one generated for you when you create an app in xCode. All you need is some basic programming knowledge to understand the design.

The specific request I got was for help dragging one UI Label and dropping it on another UI Label. That means a dragging source and a dropping location is needed and both of these will need to be NSTextFields, the class-type of Labels created in MacOS storyboards. These don’t come with any built-in drag-and-drop behavior so we will need to add that in. NSTextFields do have a default behavior for other mouse events like mouseEntered, mouseExited, mouseDown, etc. This example modifies those basic behaviors.

To modify and add these behaviors all that is needed is the fundamental Object Oriented concept of inheritance. In other words, you will need to extend NSTextField. I decided to create two extensions called DraggableLabel and DroppableLabel. You can drag an instance of DraggableLabel and drop it on an instance of DroppableLable after you are done creating the code.

The entire code set is available in my gitHub repo. You may want to get it now before reading the rest of this posting.

Before getting into the code let’s take a look at the storyboard.

I placed two Labels on the view by dragging them on and changing the text to read “Draggable” and “Target”. You can also see the “Draggable” label’s class, in the upper right hand corner, has been changed to be DraggableLabel. What you can’t see in this picture is the “Target” label’s class has also been changed. It is DroppableLabel.

That’s it. Now all we need to do is create the code for Draggablelabel and DroppableLabel. Let’s start with Draggablelabel since it is where the behavior starts and also requires the most code.

Since NSTextField is part of the Cocoa library Cocoa must be imported. Also, to get this to work DraggableLabel must inherit from NSTextField and adopt two protocols, NSDraggingSource and NSPasteboardItemDataProvider.
import Cocoa

class DraggableLabel:NSTextField, NSDraggingSource, NSPasteboardItemDataProvider{
.
.
}

Let’s start with the only override of behavior needed for NSTextField and create a mouseDown method for DraggableLabel. Initially, these four lines of code look worse than they are. It will be a lot easier if you remember that the clipboard (NSPasteboard) is used for the data transfer during dragging.

Let’s break down the lines of code you see below. I’ve numbered them to make it easier to refer to each of them.

Line 1 creates an instance of an NSPasteboardItem that will eventually be used to create the data included in the drag operation.

When the user drags a UI item they don’t actually drag a real UI item. Instead, they drag an image of the item. So in our case the type will be tiff. Remember, this isn’t the type of the data being moved during the drag it is the type of the ‘thing’ the user sees. This is why line 2 sets the item type to tiff for the NSPasteboardItem instance. Line 2 also passes self to the setDataProvider method. This is only possible because DraggableLabel adopted the NSPasteboardItemDataProvider protocol.

Line 3 creates a NSDraggingItem. This is what the user will see move across the screen during the drag. Because of this the draggingItem needs the pasteboardItem to be able to generate the appropriate tiff image. That is why it is included in this initialization call.

import Cocoa

class DraggableLabel:NSTextField, NSDraggingSource, NSPasteboardItemDataProvider{
    override func mouseDown(with theEvent: NSEvent) {

1:        let pasteboardItem = NSPasteboardItem()
2:        pasteboardItem.setDataProvider(self, forTypes: [NSPasteboard.PasteboardType.tiff])

3:        let draggingItem = NSDraggingItem(pasteboardWriter: pasteboardItem)
4:        draggingItem.setDraggingFrame(self.bounds, contents:takeSnapshot())

5:        beginDraggingSession(with: [draggingItem], event: theEvent, source: self)
    }
.
.
.
}

Line 4 takes a ‘snapshot’ of a region, the dragging frame, of the screen and uses this as the image the user will see as they do the drag. I’ve created a helper method, takeSnapshot, to make this easier to read. We’ll look at it later.

Line 5 uses the beginDraggingSession method that is part of all NSView instances to start the drag. It requires an array of ‘things’ to drag since you may want the user to drag multiple items. In this case there is just one thing to drag and that is the draggingItem instance created in line 4. It also requires us to pass along the mouse event, and, now the most important part, self is passed. By passing self to the dragging session a reference to it becomes available in the code that handles the drop event.

The takeSnapshot helper method is, I think, self explanatory and is included here for completeness. It captures an image of what is on the screen within the bounds of the DraggableLabel.

func takeSnapshot() -> NSImage {
    let pdfData = dataWithPDF(inside: bounds)
    let image = NSImage(data: pdfData)
    //if image is not nil return it else generate empty NSImage
    return image ?? NSImage()
}

There are still two small methods needed. The first is required because DraggableLabel adopted the NSPasteboardItem protocol. It has to be here, but doesn’t need to do anything for this example.


func pasteboard(_ pasteboard: NSPasteboard?, item: NSPasteboardItem, provideDataForType type: NSPasteboard.PasteboardType) {

}

The second method is required by any class adopting the NSDraggingSource protocol.

func draggingSession(_ session: NSDraggingSession, sourceOperationMaskFor context: NSDraggingContext) -> NSDragOperation {
    return NSDragOperation.generic
}

The only line of code to write for this function returns the type of dragging to be done. In our case generic was selected. There are many other options. By selecting generic we’ve allowed the drop location to decide how to handle the data. We could have selected move, delete, or other options at this point, but I didn’t want to. Instead, by selecting generic I could have different behaviors depending on which UI item it is dropped on. I like that flexibility.

You have now seen most of the code. The worst part is behind you. Now let’s take a look at the code for the DroppableLabel class.

DroppableLabel also inherits from NSTextField. It won’t add any new methods or adopt any protocols. It overrides only three methods of NSTextField to get the behavior wanted.

The first function to be overridden is awakeFromNib. This method of all NSObject instances is called when the storyboard is being interpreted and the objects declared there are being created by the run-time system for MacOS applications. This happens before the user sees anything on the screen.

awakeFromNib is overridden here so the DroppableLabel will be able to accept tiff type items dropped on it. This is done using the registerForDraggedTypes method of all NSView instances such as DroppableLable.

import Cocoa

class DroppableLabel:NSTextField{

    override func awakeFromNib() {
        registerForDraggedTypes([NSPasteboard.PasteboardType.tiff])
    }
    override func draggingEntered(_ sender: NSDraggingInfo) -> NSDragOperation {
        guard sender.draggingSource() is DraggableLabel else{
            return [];
        }
        return .copy
    }

    override func performDragOperation(_ sender: NSDraggingInfo) -> Bool {
        guard sender.draggingSource() is DraggableLabel else{
            return false;
        }
        print("You dropped \((sender.draggingSource() as! DraggableLabel).stringValue) on me!")
        return true
    }
}

The second overridden method is draggingEntered, a method of all instances of NSDraggingDestination. Since NSTextField already inherits from NSDraggingDestination we can override the method without having to do inheritance ourselves. The important part of this method is the last one, “return .copy”. Here is where we declare that the data is to be copied instead of deleted, linked to, or the other available options when something is dropped on this instance of DroppableLabel. This is needed here because generic was selected as the drag operation type in the implementation of DraggableLabel.

The last method to override, performDragOperation, is another method of NSDraggingDestination. Here the source added back in the call to beginDraggingSession is accessed and used. The source method parameter is an instance of NSDraggingInfo generated by the call to beginDraggingSession. It contains a reference to the instance of DraggableLabel.

In both of these methods, I’ve included a guard to make sure that only DraggableLables are dealt with in the code. You don’t have to have this check though I would suggest checking the types of items dropped to make sure you handle them correctly. If you have multiple types of items being dropped on a single type of drop location you should check to make sure the dropped item is one of the types you expect.

Well, there it is. Not a whole lot of code is needed to implement this, but it can be difficult to put all the parts together from the existing examples out there on the web.

I’ve uploaded this example to my gitHub repo.

Also, all links are to Apple’s official documentation. Hopefully they’ll be stable. 🙂

Advertisement

%d bloggers like this: