iOS 11 has elevated iOS, in particular for the iPad, into a true multi-tasking platform, thanks to Drag and Drop. This promises to blur the boundaries between apps, allowing content to be shared easily. Taking advantage of multi-touching, iOS 11 enables content to be moved in a natural and intuitive manner, bringing Apple’s mobile devices closer to parity with the richness enjoyed by its desktop and laptop users.
This long-awaited feature allows you to drag items to another location in the same application, or to another application. This works either through a split-screen arrangement or via the dock, using one continuous gesture. Moreover, users aren’t restricted to just selecting single items, but can drag multiple items at the same time. Many apps, including system apps such as Photos and Files, take advantage of multi-selecting and dragging multiple files.
Objectives of This Tutorial
This tutorial will introduce you to drag and drop, and then dive deeper into the architecture and strategy of using the new Drag and Drop SDK in a table view-powered app. I want to help developers such as yourself to conform your apps to the emerging UI behavior that will become standard in future iOS apps.
In this tutorial, we will cover the following:
- Understanding Drag and Drop
- Implementing Drag and Drop in a Table View
- Drag and Drop Best Practices
In the second half of this tutorial, we will go through the practical steps of enabling a simple table view app to take advantage of drag and drop, starting with one of Apple’s default table-view templates that’s available when you create a new project in Xcode 9. Go ahead and clone the tutorial’s GitHub repo if you want to follow along.
Assumed Knowledge
This tutorial assumes you have experience as an iOS developer and have used UIKit libraries in Swift or Objective-C, including UITableView
, and that you have some familiarity with delegates and protocols.
Understanding Drag and Drop
Using Apple’s nomenclature, a visual item is dragged from the source location and dropped on the destination location. This is called a drag activity, with the activity either taking place within a single app (iPad and iPhone supported), or across two apps (only available on iPad).
While a drag session is in progress, both the source and destination apps are still active and run as normal, supporting user interactions. In fact, unlike macOS, iOS supports multiple simultaneous drag activities by using multiple fingers.
But let’s focus on a single drag item, and how it uses a promise as a contract for its data representations.
Drag Items as Promises
Each drag item can be thought of as a promise, a contained data representation that will be dragged and dropped from a source to its destination. The drag item uses an item provider, populating its registeredTypeIdentifiers
with uniform type identifiers, which are individual data representations that it will commit to deliver to its intended destination along with a preview image (which is pinned under the user’s touch point visually), as illustrated below:
The drag item is constructed through the UIDragInteractionDelegate
from the source location and handled on the destination location via the UIDropInteractionDelegate
. The source location must conform to the NSItemProviderWriting
protocol, and the destination location must conform to the NSItemProviderReading
protocol.
That’s a basic overview of nominating a view as a drag item, through promises. Let’s see how we implement a drag source from a view, before establishing the drop destination.
Implementing a Drag Source
Focusing our attention on the first part of the drag & drop—the drag source—we need to accomplish the following steps when the user initiates a drag activity:
- Conform the view to the
UIDragInterationDelegate
protocol. - Declare the data items that will form the item promise, via
dragInteraction(_:itemsForBeginning:)
. - Populate the drag session with the drag items in preparation for the user to drag the items to the drop destination.
The first thing you will need to do is conform your nominated view to the UIDragInterationDelegate
protocol, by creating a new UIDragInteraction
instance and associating it with your ViewController
’s view’s addInteraction
property as well as its delegate, as follows:
let dragInteraction = UIDragInteraction(delegate: dragInteractionDelegate) view.addInteraction(dragInteraction)
After declaring your drag source, you proceed to creating a drag item, essentially a promise of data representation, by implementing the delegate method dragInteraction(_:itemsForBeginning:)
, which the system calls to return an array of one or more drag items to populate the drag session’s items property. The following example creates an NSItemProvider
from an image promise, before returning an array of data items:
func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] { guard let imagePromise = imageView.image else { return [] //By returning an empty array you disable dragging. } let provider = NSItemProvider(object: imagePromise) let item = UIDragItem(itemProvider: provider) return [item] }
The delegate method above responds to a drag request triggered when the user commences dragging the item, with the Gesture Recognizer (UIGestureRecognizer
) sending a “drag started” message back to the system. This is what essentially initializes the “drag session”.
Next, we proceed with implementing the drop destination, to handle the array of drag items initiated in the session.
Implementing a Drop Destination
Likewise, to conform your nominated view to accept and consume data as part of the drop destination, you will need to complete the following steps:
- Instantiate a
DropInteraction
. - Declare the data item types (if any) you will accept, using
dropInteraction(_:canHandle:)
. - Implement a drop proposal using the
dropInteraction(_:sessionDidUpdate:)
protocol method, stating whether you will be copying, moving, refusing, or canceling the session. - Finally, consume the data items using the
dropInteraction(_:performDrop:)
protocol method.
Just as we configured our view to enable drag, we will symmetrically configure our nominated view to accept dropped items from a drag session, using the UIDropinteractionDelegate
and implementing its DropInteraction
delegate method:
let dropInteraction = UIDropInteraction(delegate: dropInteractionDelegate) view.addInteraction(dropInteraction)
To designate whether a View is capable of accepting drag items or is refusing, we implement the dropInteraction(_:canHandle:)
protocol method. The following method lets our view tell the system whether it can accept the items, by stating the type of objects it can receive—in this case UIImages.
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { // Explicitly state the acceptable drop item type here return session.canLoadObjects(ofClass: UIImage.self) }
If the view object does not accept any drop interactions, you should return false from this method.
Next, bind a drop proposal in order to accept data from the drop session. Although this is an optional method, it is highly recommended that you do implement this method, since it provides visual cues as to whether the drop will result in copying the item, moving it, or whether the drop will be refused altogether. By implementing the dropInteraction(_:sessionDidUpdate:)
protocol method, which returns a UIDropProposal
, you indicate the proposal type using the specific operation enumeration type (UIDropOperation
). The valid types you could return are:
cancel
forbidden
copy
move
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { // Signal to the system that you will move the item from the source app (you could also state .copy to copy as opposed to move) return UIDropProposal(operation: .move) }
And finally, to consume the data item content within your destination location, you implement the dropInteraction(_:performDrop:)
protocol method in the background queue (rather than in the main queue—this ensures responsiveness). This is illustrated below:
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { // Consume UIImage drag items session.loadObjects(ofClass: UIImage.self) { items in let images = items as! [UIImage] self.imageView.image = images.first } }
We have demonstrated how you would implement drag and drop in a custom view, so now let’s move on to the practical part of this tutorial and implement drag and drop on an app with a table view.
Implementing Drag and Drop in a Table View
So far, we have been discussing how to implement drag and drop in custom views, but Apple has also made it easy to augment table views and collection views with drag and drop. Whereas text fields and views automatically support drag and drop out of the box, table and collection views expose specific methods, delegates, and properties for customizing their drag and drop behaviors. We’ll take a look at this shortly.
Start by creating a new project in Xcode 9, ensuring you select Master-Detail App from the template window:
Before you start working on the rest of the steps, go ahead and build and run the project and play around with it a bit. You’ll see that it generates a new timestamp date when you select the plus (+) button. We are going to improve on this app by allowing the user to drag and order the timestamps.
Drag and drop is supported in table views (as well as collections) through specialized APIs that enable dragging and dropping with rows, by conforming our table view to adopt both the UITableViewDragDelegate
and UITableViewDropDelegate
protocols. Open up the MasterViewController.swift file and add the following to the viewDidLoad()
method:
override func viewDidLoad() { super.viewDidLoad() ... self.tableView.dragDelegate = self self.tableView.dropDelegate = self ...
As we did with custom views, we need to handle the new drag session when the user drags a selected row or multiple rows/selections. We do this with the delegate method tableView(_:itemsForBeginning:at:)
. Within this method, you either return a populated array which commences dragging of the selected rows, or an empty array to prevent the user from dragging content from that specific index path.
Add the following method to your MasterViewController.swift file:
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { let dateItem = self.objects[indexPath.row] as! String let data = dateItem.data(using: .utf8) let itemProvider = NSItemProvider() itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypePlainText as String, visibility: .all) { completion in completion(data, nil) return nil } return [ UIDragItem(itemProvider: itemProvider) ] }
Some of the code added should already be familiar to you from the previous section, but essentially what we are doing is to create a data item from the selected object, wrap it in an NSItemProvider
, and return it in a DragItem
.
Turning our attention next to enabling the drop session, proceed with adding the following two methods:
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { return session.canLoadObjects(ofClass: NSString.self) } func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { if tableView.hasActiveDrag { if session.items.count > 1 { return UITableViewDropProposal(operation: .cancel) } else { return UITableViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath) } } else { return UITableViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath) } }
The first method tells the system that it can handle String data types as part of its drop session. The second delegate method, tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
, tracks the potential drop location, notifying the method with each change. It also displays visual feedback to let the user know if a specific location is forbidden or acceptable, using a small visual icon cue.
Lastly, we handle the drop and consume the data item, calling tableView(_:performDropWith:)
, fetching the dragged data item row, updating our table view’s data source, and inserting the necessary rows into the table.
func tableView(_ tableView: UITableView, performDropWith coordinator: UITableViewDropCoordinator) { let destinationIndexPath: IndexPath if let indexPath = coordinator.destinationIndexPath { destinationIndexPath = indexPath } else { // Get last index path of table view. let section = tableView.numberOfSections - 1 let row = tableView.numberOfRows(inSection: section) destinationIndexPath = IndexPath(row: row, section: section) } coordinator.session.loadObjects(ofClass: NSString.self) { items in // Consume drag items. let stringItems = items as! [String] var indexPaths = [IndexPath]() for (index, item) in stringItems.enumerated() { let indexPath = IndexPath(row: destinationIndexPath.row + index, section: destinationIndexPath.section) self.objects.insert(item, at: indexPath.row) indexPaths.append(indexPath) } tableView.insertRows(at: indexPaths, with: .automatic) } }
Further Reading
For more information on supporting drag and drop in your table views, consult Apple’s very own developer documentation on Supporting Drag and Drop in Table Views.
Drag and Drop Best Practices
The content we covered should help you implement drag and drop in your apps, enabling users to visually and interactively move around content within their existing apps, as well as across apps.
Along with the technical knowledge of how to implement drag and drop, however, it is imperative that you take the time to consider how you would implement drag and drop, following the best practices advocated by Apple in their Human Interface Guidelines (HIG), in order to provide users with the best possible iOS 11 user experience.
To wrap up, we’ll touch on some of the most important aspects to consider, starting with visual cues. According to the HIG, the fundamental experience with drag and drop is that when a user interacts with some content, visual cues indicate to the user an active drag session, denoted by having the content element rise, along with a badge to indicate when dropping is or isn’t possible.
We have already used this best practice in our earlier examples, when we included the tableView(_:dropSessionDidUpdate:withDestinationIndexPath:)
method, stating whether the drop destination is a move, copy, or forbidden. You should ensure with custom views and interactions that you maintain the expected set of behaviors that other iOS 11 apps, especially system apps, support.
Another important aspect to consider is deciding whether your drag session will result in a move or copy. As a general rule of thumb, Apple suggests that when you work within the same app, it should generally result in a move, whereas it makes more sense to copy the data item when you are dragging between different apps. While there are exceptions, of course, the underlying principle is that it should make sense to the user, and what they expect should happen.
You should also think in terms of sources and destinations, and whether it makes sense to drag something or not.
Let’s take a look at some of Apple’s own system utilities. Notes, for example, allows you to select and drag text content to other locations within the app, or across to other apps on the iPad, via split-screen. The Reminders app allows you to move reminder items from one list to another list. Think in terms of functionality when deciding on how users use your content.
Apple’s guidance is that all content that is editable should support accepting dropped content, and any content that is selectable should accept draggable content, in addition to supporting copy and paste for those types of elements. By leveraging standard system text views and text fields, you will get support for drag and drop out of the box.
You should also support multi-item drag and drop, as opposed to only supporting single items, whereby users can use more than one finger to select multiple items concurrently, stacking the selected items into a group to be dropped into their intended destinations. An example of this in action is selecting multiple pictures in the Photos app, or multiple files in the Files app.
A final guideline is to provide users with the ability to reverse an action, or “undo” a drop. Users have been accustomed for a very long time to the concept of undoing an action in most of the popular apps, and drag and drop should be no exception. Users should have the confidence to be able to initiate a drag and drop and be able to reverse that action if they drop the element in the wrong destination.
Further Reading
There are many more drag and drop best practice guidelines beyond what we’ve looked at, including how to support dropped visual indicator cues, displaying failed dropped actions, and progress indicators for non-instant drag sessions, such as data transfers. Consult Apple’s iOS Human Interface Guidelines on drag and drop, for the complete list of best practices.
Conclusion
In this tutorial, you’ve learned how to enrich your iOS apps with drag and drop, courtesy of iOS 11. Along the way, we explored how to enable both custom views and table views as drag sources and drop destinations.
As part of the iOS evolution towards a more gesture-driven user interface, no doubt drag and drop will rapidly become an expected feature for users system-wide, and as such, all third-party apps should also conform. And just as important as implementing drag and drop, you will need to implement it right, so that it becomes second nature to users, encompassing simplicity and functionality.
And while you’re here, check out some of our other posts on iOS app development!
-
iOS SDKUpdating Your App for iOS 11
-
iOS SDKPassing Data Between Controllers in Swift
-
XcodeWhat’s New in Xcode 9?
Powered by WPeMatico