Adding drag and drop to a UICollectionView

A lot of iOS apps make extensive use of collections and tables. Therefore, it makes a lot of sense that, whenever Apple introduces a huge feature such as drag and drop, they take a step back and evaluate how the feature should work for collections or tables. Luckily, drag and drop was no exception and Apple truly put some thought into making drag and drop work great.

In this section, you'll implement drag and drop for the collection of images that is at the bottom of the screen for the ARGallery app you created in Chapter 12, Using Augmented Reality. You will implement the following features that use drag and drop for UICollectionView:

  • Dragging photos from the collection into the AR viewport.
  • Reordering items in the collection view.
  • Adding items from external sources to the collection view.

As a bonus, you will make sure that the first two features also work on an iPhone since these features are only used inside the app so they can be used on iPhone. Since Apple has tailored drag and drop to work perfectly with UICollectionView, the basic concepts for drag and drop still apply; you only have to use slightly different protocols. For instance, instead of implementing UIDragInteractionDelegate, you implement UICollectionViewDragDelegate.

The first feature, dragging photos from the collection of images to the AR Gallery, is implemented similarly to the drag and drop experience you implemented before. You will implement the relevant protocols first, and then you will implement the interactions. The code bundle for this chapter contains a slightly modified version of the ARGallery that you built before. The modifications allow you to focus on implementing drag and drop instead of having to make minor adjustments to the existing code.

Since you should be familiar with the dropping implementation already, add the following extension to ViewController.swift in AugmentedRealityGallery:

extension ViewController: UIDropInteractionDelegate {
  func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
    return UIDropProposal(operation: .copy)
  }

  func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
    guard let itemProvider = session.items.first?.itemProvider,
      itemProvider.canLoadObject(ofClass: UIImage.self)
      else { return }

    itemProvider.loadObject(ofClass: UIImage.self) { [weak self] item ,error in
      guard let image = item as? UIImage
        else { return }

      DispatchQueue.main.async {
        self?.addImageToGallery(image)
      }
    }
  }
}

Nothing crazy happens in this snippet. In fact, it's so similar to the code you have seen already that you should be able to understand what happens in this snippet on your own. The next step is to implement the UICollectionViewDragDelegate protocol. Add the following extension to ViewController.swift:

extension ViewController: UICollectionViewDragDelegate {
  func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
    let image = UIImage(named: images[indexPath.row])!
    let itemProvider = NSItemProvider(object: image)

    return [UIDragItem(itemProvider: itemProvider)]
  }
}

The preceding implementation serves the same purpose as UIDragInteractionDelegate. The main difference is that you have access to the IndexPath of the item that was selected for dragging. You can use IndexPath to obtain the dragged image and create a UIDragItem for it. Let's set up the interaction objects for this part of the app now. Add the following lines of code to viewDidLoad():

collectionView.dragDelegate = self
collectionView.dragInteractionEnabled = true

let dropInteraction = UIDropInteraction(delegate: self)
arKitScene.addInteraction(dropInteraction)

If you build and run the app now, you can drag photos into the AR gallery from the collection view at the bottom. If you run the app on an iPad, you are even able to drag images from external apps into the AR Gallery! This is quite awesome, but you're not done yet. Let's allow users to drag items from external apps into the collection, so they have easy access to it. And while we're at it, let's implement the reordering of the collection using drag and drop as well.

To implement the reordering of items in the collection view and to allow users to add external images to their collection, you must implement UICollectionViewDropDelegate. As you will see soon, it is possible to distinguish between a drop session that originated from within the app or outside the app. This information can be used to determine whether the user wants to reorder the collection or add an item to it. Add the following extension for ViewController:

extension ViewController: UICollectionViewDropDelegate {
  func collectionView(_ collectionView: UICollectionView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UICollectionViewDropProposal {

    if session.localDragSession != nil {
      return UICollectionViewDropProposal(operation: .move, intent: .insertAtDestinationIndexPath)
    }

    return UICollectionViewDropProposal(operation: .copy, intent: .insertAtDestinationIndexPath)
  }
}

The preceding snippet implements collectionView(_:dropSessionDidUpdate:withDestinationIndexPath:). This delegate method is pretty similar to dropInteraction(_:sessionDidUpdate:) except you also have access to the destination index path that the user is currently hovering their finger over. By checking whether there is a localDragSession on a UIDropSession, you can detect whether the user wants to reorder the collection or whether they are adding an item from an external source. By specifying an intent on the drop proposal, CollectionView knows how it should update its interface to visualize the action that is taken when the user performs the drop. Speaking of performing drops, add the following code to the UICollectionViewDropDelegate extension you just added:

unc collectionView(_ collectionView: UICollectionView, performDropWith coordinator: UICollectionViewDropCoordinator) {
  switch coordinator.proposal.operation {
  case .copy:
    performCopy(forCollectionView: collectionView, with: coordinator)
  case .move:
    performMove(forCollectionView: collectionView, with: coordinator)
  default:
    return
  }
}

This method is relatively simple. Depending on the proposal that is used, a different method is called. You will add the copy and move methods soon, but first, let's talk a little bit about UICollectionViewDropCoordinator. UICollectionViewDropCoordinator contains information about the items that are being dragged, the animations that should be performed, the drop proposal, and of course the drop session. When performing a drop, you make use of the coordinator to request the drag items, but also to make sure the collection view properly updates its view.

The first method you will implement is performMove(forCollectionView:with:) since it's the simpler of the two remaining methods to implement. Add the following snippet to the UICollectionViewDropDelegate extension:

func performMove(forCollectionView collectionView: UICollectionView, with coordinator: UICollectionViewDropCoordinator) {
  let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)

  guard let item = coordinator.items.first,
    let sourceIndexPath = item.sourceIndexPath
    else { return }

  let image = images.remove(at: sourceIndexPath.row)
  images.insert(image, at: destinationIndexPath.row)

  collectionView.performBatchUpdates({
    collectionView.deleteItems(at: [sourceIndexPath])
    collectionView.insertItems(at: [destinationIndexPath])
  })

  coordinator.drop(item.dragItem, toItemAt: destinationIndexPath)
}

The preceding snippet uses the coordinator to retrieve the first item in the drag session. The app doesn't support dragging multiple items at once, so this is alright. Next, the item's source index path is used to remove the image that should be moved from the array of images. The destination index path is then used to add the image back into the array of images at its new location. This is done to make sure the data source is updated before updating the collection view. After the collection view is updated, drop(_:toItemAt:) is called on the coordinator to animate the drop action.

The final method you need to implement is performCopy(forCollectionView:with:). Add the following code to the UICollectionViewDropDelegate extension:

func performCopy(forCollectionView collectionView: UICollectionView, with coordinator: UICollectionViewDropCoordinator) {
  let destinationIndexPath = coordinator.destinationIndexPath ?? IndexPath(item: 0, section: 0)

  for item in coordinator.items {
      let dragItem = item.dragItem
      guard dragItem.itemProvider.canLoadObject(ofClass: UIImage.self) else { continue }

      let placeholder = UICollectionViewDropPlaceholder(insertionIndexPath: destinationIndexPath, reuseIdentifier: "GalleryCollectionItem")
      let placeholderContext = coordinator.drop(dragItem, to: placeholder)

      dragItem.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] item, error in
          DispatchQueue.main.async {
              guard let image = item as? UIImage else {
                  placeholderContext.deletePlaceholder()
                  return
              }

              placeholderContext.commitInsertion { indexPath in
                  self?.images.insert(image, at: indexPath.row)
              }
          }
      }
  }
}

Take a close look at UICollectionViewDropPlaceholder in this snippet. This class was introduced in iOS 11, and it is used to add temporary items to CollectionView. Because it might take a little while to load data from an item provider, you need a mechanism to update the UI while you're loading data. This is the goal of using a placeholder. When you call drop(_:to:) on coordinator, you receive a placeholder context. You use this context to either remove the placeholder if loading data from the item provider failed, or to commit the insertion if it succeeds. Once it has succeeded and you commit the insertion, you must make sure to update the collection's data source by adding the image to the image array. Otherwise, your app could crash due to data-source inconsistencies.

Since a placeholder is not part of your CollectionView data source, it is essential that you proceed with caution if you have a placeholder present in your CollectionView. For instance, your placeholder will be gone when you reload CollectionView before committing or removing the placeholder.

Lastly, add the following line to the viewDidLoad of ViewController to set the collection view's drop delegate:

collectionView.dropDelegate = self

At this point, you should be able to create a very nice implementation of drag and drop in your apps. However, there is more to learn on the topic since you can customize many aspects of how drag and drop works for your app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset