Drag and Drop Tutorial for iOS

In this drag and drop tutorial you will build drag and drop support into UICollectionViews and between two separate iOS apps. By Christine Abernathy.

Leave a rating/review
Download materials
Save for later
Share

Apple introduced drag and drop functionality in iOS 11, allowing users to drag items from one screen location to another. On iPhone, drag and drop is only available within an app, whereas on iPads, it’s also available across apps. This is really handy for doing things like quickly adding images from Photos into an email.

In this tutorial, you’ll explore drag and drop by building upon CacheManager, two apps for managing geocaches:

CacheMaker organizes geocaches in a Kanban board of in-progress and completed items. CacheEditor allows users to edit the details of a geocache brought over from CacheMaker. You’ll implement these management features by adding drag and drop support to both apps.

Getting Started

Use the Download Materials button at the top or bottom of this tutorial to download the starter project. Open CacheManager.xcworkspace in Xcode and select CacheMaker as the active scheme:

Build and run CacheMaker. You should see two collection views with the first containing geocaches for in-progress work:

Try dragging a geocache from the in-progress lane to the completed lane:

Your goal in this first part of the tutorial is to get this working. Later on, you’ll unlock the ability to drag and drop geocaches to and from the CacheEditor companion app.

Take a look at the key CacheMaker files in Xcode:

  • CachesDataSource.swift: Represents the data source for a collection view of geocaches.
  • CachesViewController.swift: Displays the Kanban board for the geocaches.

These are the files you’ll be working with to add the desired functionality.

Drag and Drop Overview

When you drag items from a source app, a drag activity begins and the system creates a drag session. The source app sets up a drag item to represent the underlying data when the drag activity starts. Dropping the items in a destination app ends the drag activity.

Drag items are wrapped in an item provider which describes the data types the source app can deliver. When the items are dropped, the destination app asks for the items in the format it can consume.

Apple automatically supports drag and drop in text views and text fields. It also provides specialized APIs for table views and collection views. You can add drag and drop to custom views as well.

In this tutorial, you’ll explore drag and drop in collection views and custom views.

Adding Drag Support

Go to CachesDataSource.swift and add the following extension to the end of the file:

extension CachesDataSource {
  func dragItems(for indexPath: IndexPath) -> [UIDragItem] {
    let geocache = geocaches[indexPath.item]
    let itemProvider = NSItemProvider(object: geocache.name as NSString)
    let dragItem = UIDragItem(itemProvider: itemProvider)
    return [dragItem]
  }
}

Here you create an item provider from the NSString representation of the geocache name. You then return an array of one drag item which wraps this item provider.

Next, open CachesViewController.swift and add the following to the end of the file:

extension CachesViewController: UICollectionViewDragDelegate {
  func collectionView(_ collectionView: UICollectionView,
                      itemsForBeginning session: UIDragSession,
                      at indexPath: IndexPath) -> [UIDragItem] {
    let dataSource = dataSourceForCollectionView(collectionView)
    return dataSource.dragItems(for: indexPath)
  }
}   

You adopt UICollectionViewDragDelegate and implement the required method that’s called when a drag activity starts. Your implementation gets the data source for the collection view, then returns the corresponding drag items for the selected item.

Add the following to viewDidLoad() after the collection view delegate assignment:

collectionView.dragDelegate = self  

This makes the view controller the drag delegate.

Build and run the app. Tap and hold on a collection view cell representing a geocache. The tapped cell should rise up allowing you to drag it around:

Note that although you can drag an item around, you can’t drop it anywhere. Attempting to do so simply drops it back where it started.

Open Reminders in Split View next to CacheMaker. You should be able to drag a geocache and drop it into Reminders:

Reminders can accept the exported NSString representation of the geocache name and use it to create a new reminder.

Now try dragging any text from Reminders into CacheMaker. Nothing happens. That’s because you haven’t added drop support to CacheMaker. You’re going to tackle this next.

Adding Drop Support

Go to CachesDataSource.swift and add the following to the CachesDataSource extension:

func addGeocache(_ newGeocache: Geocache, at index: Int) {
  geocaches.insert(newGeocache, at: index)
}   

This adds a new geocache to the data source.

Switch over to CachesViewController.swift and add the following protocol extension to the end:

extension CachesViewController: UICollectionViewDropDelegate {
  func collectionView(
      _ collectionView: UICollectionView, 
      performDropWith coordinator: UICollectionViewDropCoordinator) {
    // 1
    let dataSource = dataSourceForCollectionView(collectionView)
    // 2
    let destinationIndexPath =
      IndexPath(item: collectionView.numberOfItems(inSection: 0), section: 0)
    // 3
    let item = coordinator.items[0]
  // 4
   switch coordinator.proposal.operation
      {
        case .copy:
          print("Copying...")
          let itemProvider = item.dragItem.itemProvider
         // 5
          itemProvider.loadObject(ofClass: NSString.self) { string, error in
            if let string = string as? String {
              // 6
              let geocache = Geocache(
                name: string, summary: "Unknown", latitude: 0.0, longitude: 0.0)
              // 7
              dataSource.addGeocache(geocache, at: destinationIndexPath.item)
              // 8
              DispatchQueue.main.async {
                collectionView.insertItems(at: [destinationIndexPath])
              }
            }
          }
        default:
          return
      }
  } 
}

Here you adopt the UICollectionViewDropDelegate protocol. You then implement the required method that’s called when the user ends a drag activity. Your implementation:

  1. Gets the data source for the collection view.
  2. Sets the end of the collection view as the item drop destination.
  3. Selects the first drag item.
  4. Checks how you propose to handle the drop.
  5. Asynchronously fetches the dragged item’s data.
  6. Creates a new geocache with a name based on the incoming string data.
  7. Adds the new geocache to the data source.
  8. Inserts the new item in the collection view. You invoke this on the main thread since the data fetching completion block runs on an internal queue.

Responding to Drops

Add the following to the end of the UICollectionViewDropDelegate extension:

func collectionView(
    _ collectionView: UICollectionView,
    dropSessionDidUpdate session: UIDropSession,
    withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UICollectionViewDropProposal {
  if session.localDragSession != nil {
    return UICollectionViewDropProposal(operation: .forbidden)
  } else {
    return UICollectionViewDropProposal(
      operation: .copy,
      intent: .insertAtDestinationIndexPath)
  }
}

You specify the response to an item being dragged. This includes providing visual feedback to the user.

The code here forbids drag-and-drops within the app. It proposes copy operations for items dropped from another app.

Add the following to viewDidLoad() after the drag delegate is assigned:

collectionView.dropDelegate = self  

This sets the view controller as the drop delegate.

Build and run the app. With Reminders in Split View, verify that you can drag a reminder into the in-progress collection view:

If you try to drop into the middle of the list, you’ll see it only adds to the end of the list. You’ll improve this later.

Try dragging and dropping a geocache inside the app. Verify that you get a visual cue that this is disallowed:

That isn’t ideal, so you’ll work on it next.