Drag and drop in iOS 11 - a quick start

12 June 2017

The most significant announcement at WWDC 2017 (at least as far as UIKit is concerned) was drag-and-drop. It’s been possible to mimic this to an extent with UICollectionView, but what was announced at WWDC is the full-fat, cross-application, deeply-system-integrated real deal. It’s now possible to perform sophisticated multi-touch drag and drop operations across the whole iOS platform, which is going to open up a world of UI possibilities in upcoming apps.

Like much of iOS, the basic concepts are relatively straight-forward, but there’s a great deal of power and complexity hidden in the APIs. On the basis that the best way to understand something is to play with it, here’s a minimum viable drag and drop app that implements the very basics.

simulator

The core concepts

There’s three parts to drag and drop - unsurprisingly, the first two are the drag source, and the drop destination. These are UIViews that have UIDropInteraction and UIDragInteractions attached to them. Through the relevant delegate methods, the drag source and drop destination share instances of UIDropSession, which act as the “transport mechanism” for the data that the drag and drop represents.


       UIView                                            UIView
          |                                                 |
          |                                                 |
  UIDragInteraction                                 UIDropInteraction
          |                                                 |
          |                                                 |
UIDragInteractionDelegate ---> UIDropSession ---> UIDropInteractionDelegate
                                     |
                                     |
                                [UIDragItem]
                                     |
                                     |
                               NSItemProvider

To make that more concrete, think of a use case like copying a photo between two albums. The albums might be represented in the app’s UI as a series of UIImageViews containing thumbnails. Each UIImageView could have a UIDragInteraction attached to it - the UIDragInteractionDelegate would create a UIDropSession that contains a references to the photo being dragged. When the user drags over a view with a UIDropInteraction attached, the UIDropInteractionDelegate will decide whether it can accept the type of data contained in the UIDropSession. If it can, the photo is “exchanged” between the two albums by unpacking it from the UIDropSession and updating the underlying model of the album.

The example

The example project can be downloaded from https://github.com/timd/DragAndDropExample. It’s incredibly simple - there are two image thumbnails on the left hand side of the screen that can be dragged and dropped over on the gray box on the right hand side.

The images on the left have UIDragInteractions attached to them, and the grey box has a UIDropInteraction attached.

Implementing dragging

Implementing dragging for a view has two steps: creating and adding a drag interaction; and implementing the single required UIDragInteractionDelegate method

Creating and adding a drag interaction

Creating and adding a drag interaction follows exactly the same pattern as the familiar UIInteraction - create an instance of UIDragInteraction with a delegate, and add it to the view:

    override func viewDidLoad() {
        super.viewDidLoad()
        
        let firstDragInteraction = UIDragInteraction(delegate: self)
        firstDraggableView.addInteraction(firstDragInteraction)
        
    }

Implementing the drag interaction delegate

The UIDragInteractionDelegate can be any class that conforms to the protocol, but to keep things simple here we’re using the UIViewController class. To conform to the protocol requires one function:

func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem]

This takes the UIDragInteraction and the UIDragSession and returns an array of UIDragItems - these are representations of the data that the views represent. In this example, they are the UIImageViews containing the thumbnails.

    func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
        
        var imageItemProvider: NSItemProvider
        
        // Figure out which view is being dragged, and load the appropriate image into the item provider
        if interaction.view == firstDraggableView {
            imageItemProvider = NSItemProvider(object: UIImage(named: "panda_200x200")!)
        } else {
            imageItemProvider = NSItemProvider(object: UIImage(named: "kath_square")!)
        }
        
        // Create and return a drag item containing the item provider
        let dragItem = UIDragItem(itemProvider: imageItemProvider)
        return [dragItem]
        
    }

Working through this function, the first thing we do is to declare an NSItemProvider. This will contain the UIImage that is being dragged - depending on which view is being touched, we create it from the file. With the NSItemProvider containing the UIImage, we can then create the UIDragItem and return that in an array.

Note that this is definitely not the optimal solution - in a real world application, it’s far more likely that you’d encapsulate a reference to the underlying model of the image in the NSItemProvider, and pass that around. But to keep things simple, here we’re passing the actual image itself.

This is all that’s needed to get the drag working - long-press on the image, and UIKit will handle the animation of the image “lifting” and following the user’s finger around.

Creating and adding a drop interaction

With the drag in progress, it’s time to implement the drop side of things. The process is almost identical to the drag - create an instance of a UIDropInteraction with a UIDropInteractionDelegate, and attach it to the view that will accept the drop:

// Create the drop interaction and attach to the drop target
let dropInteraction = UIDropInteraction(delegate: self)
droppableView.addInteraction(dropInteraction)

Implementing the drop interaction delegate

There’s a bit more to do in order to implement the drop interaction delegate. The first step is to inform the drag interaction whether the view is prepared to accept the data that’s on offer. This is handled by the dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) function, which will be called every time the drag touch moves over the drop target view:

    // Declare that the target view can handle UIImages
    func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
        return session.canLoadObjects(ofClass: UIImage.self)
    }

Here we’re saying that this drop target will only accept UIImage objects. If the UIDragItem contains representations of something else - strings, structs, model classes etc - this function will return false and an attempt to drop here will be cancelled.

The second UIDropInteractionDelegate method that we need to implement tells the drag interaction what type of drop operation we are willing to support. There are four options:

  • cancel, which prevents the drop taking place
  • copy, which will indicate that the data will be copied from the source to destination
  • move, which indicates that the underlying model item will move from source to destination
  • forbidden, which is an edge case of cancel and applies a badge to the destination view to show that dropping is not allowed here. An example where you’d use this is if you attempt to drop an image onto a read-only album.

In this example we’re keeping it simple, so we return .copy:

    // Items will be pasted
    func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
        return UIDropProposal(operation: .copy)
    }

By this point we’ve decided whether the drop can take place, and what kind of action should be performed - now it’s time to for the third UIDropInteractionDelegate method to do the heavy lifting of handling the drop.

This is the dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) function:

    func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
        session.loadObjects(ofClass: UIImage.self) { imageItems in
            let droppedImages = imageItems as! [UIImage]
            self.imageDropView.image = droppedImages.first
        }
    }

Here we’re using a convenience method on the UIDropSession to extract all the objects of the UIImage class (remember that’s what we decided the drop target would accept). The session.loadObjects method takes a closure - in this we’re casting the imageItems into an array of UIImages and then updating the imageDropView with the first one.

Again, this is a non-optimal implementation for the sake of clarity - it’s far more likely in a production app that data will be loaded from a data source somewhere - in which case the session would contain representations of the data, rather than the data itself.

Some final polish

Although this is a minimum viable implementation, there’s a couple of additional UIDragInteractionDelegate methods that we can use to polish things up a bit. The first is dragInteraction(_ interaction: UIDragInteraction, sessionWillBegin session: UIDragSession) - this gets invoked as the drag session starts. We can use this to dim the image being ‘dragged’:

    // Dim the dragged view when the session starts
    func dragInteraction(_ interaction: UIDragInteraction, sessionWillBegin session: UIDragSession) {
        interaction.view?.alpha = 0.1
    }

There’s also a corresponding dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, didEndWith operation: UIDropOperation) which is called at the end of the drag session: this is a good place to reverse the dimming:

    // Fade the dragged image back in once the drag session has finished
    func dragInteraction(_ interaction: UIDragInteraction, session: UIDragSession, didEndWith operation: UIDropOperation) {
        UIView.animate(withDuration: 1.0) {
            interaction.view?.alpha = 1.0
        }
    }

Summary

This is a very minimal implementation of drag and drop - it’s contained within a single app, doesn’t interact with the system at all, and doesn’t manipulate any kind of underlying data structure. Nevertheless, it implements the basic building blocks of the drag and drop API, so you can use this as a starting point for much more sophisticated interactions.