Advanced Git, Second Edition

Git is key to great version control and collaboration on software projects.
Stop struggling with Git and spend more time on the stuff that matters!

Home iOS & Swift Tutorials

UICollectionView Tutorial: Headers, Selection and Reordering

Learn how to implement reusable views for UICollectionView section headers, select cells and reorder with drag and drop.

5/5 5 Ratings

Version

  • Swift 5, iOS 14, Xcode 12
Update note: Fabrizio Brancati updated this tutorial for iOS 14.4, Swift 5.3 and Xcode 12.4. Mark Struzinski wrote a previous update, and Brandon Trebitowski wrote the original.

UICollectionView is one of the most important classes in UIKit, used to display data to a user in a customized layout. An example of such a layout is the iOS Photos app, which shows your data ordered in the grid layout and allows you to select an image and share or reposition it in your grid view.

Like UITableView, UICollectionView can contain both homogeneous and heterogeneous collections of views embedded inside a UICollectionViewCell. You use a layout to define how the cells are laid out. These can be either chosen from pre-existing layouts or you can define your own. A delegate allows you to respond to interaction, and a data source allows you to define what data is shown in the collection view.

If you need to brush up on the basics of UICollectionView, check out UICollectionView: Getting Started.

In this tutorial, you’ll work on a photo-browsing app, FlickrSearch, similar to the iOS Photos app, in which you’ll use to learn how to:

  • Implement reusable views for section headers.
  • Interact with cells.
  • Update layout based on selection.
  • Share selected items to other apps.
  • Reorder with drag-and-drop.

Getting Started

You can download the starter project by clicking the Download Materials button at the top or bottom of this tutorial.

Open the starter project. You’ll first need to set up the Flickr API key so your app can fetch photos. First, go to the Flickr website and sign-in or create an account if you don’t already have one. Then, click APPLY FOR A NON-COMMERCIAL KEY and complete the form. Click Submit and you’ll see your unique API key.

Now, open Flickr.swift in the Xcode project and paste the key from Flickr into the apiKey string constant at the top of the file.

Build and run. In the search bar, enter a word and tap Search.

Flickr starter project photos

Adding Custom Section Headers

Play around with the starter app and you’ll observe:

  1. The app displays 20 photos related to each word search.
  2. It creates a new section in the same UICollectionView for each search you perform.

There are no section headers in the UICollectionView. So for your first task, you’ll add a new section header using the search text as the section title.

To display this section header, you’ll use UICollectionReusableView. This class is like UICollectionViewCell, except it’s usually used to display headers and footers. Like cells, UICollectionView places them in a reuse queue rather than deleting them when they scroll out of the visible bounds.

You’ll add this element directly to the storyboard and create a subclass so it can dynamically update its title.

Creating a UICollectionReusableView Class

Create a new file to represent the custom class for the header.

  • Select File ▸ New ▸ File. Select Cocoa Touch Class.
  • Choose Subclass of UICollectionReusableView.
  • Name the class FlickrPhotoHeaderView.
  • Leave Also create XIB file unchecked.
  • Select Swift as the language.
  • Click Next.

Select the starter app project’s Views folder and click Create to create the file.

Dialog box for creating new FlickrPhotoHeaderView

Next, open Main.storyboard and select the collection view in FlickrPhotosViewController. Select the Attributes inspector and, in the Accessories section, check the Section Header checkbox. Notice a Collection Reusable View appears in the storyboard underneath the Collection View.

Check section header in Collection View dialog box

Setting the Section Header View Class

Now, you’ll set your custom class as the section header view. To do this:

  • Select the Collection Reusable View added in the collection view.
  • Open the Identity inspector and set the Class in the Custom Class section to FlickrPhotoHeaderView.
  • Next, in the Attributes inspector, set the Identifier to FlickrPhotoHeaderView.
  • Also set the Background color to Opaque Separator Color to give it a nice offset from the rest of the collection view.
  • Open the Size inspector and set the Height to 90.

Now open the Object library with the key combination Command-Shift-L and drag a Label onto the reusable view. Use the Auto Layout align button at the bottom of the storyboard window to pin it to the horizontal and vertical center of the view. In the Attributes inspector of the label, update the font to System 32.

Add New Alignment Constraints dialog

Connecting the Section Header to Data

Open FlickrPhotoHeaderView.swift in an additional editor pane and Control-drag from the label in the header view over to the file and name the outlet titleLabel. It will add the following code:

class FlickrPhotoHeaderView: UICollectionReusableView {
  @IBOutlet weak var titleLabel: UILabel!
}

Finally, you need to implement a new data source method to wire everything together. Open FlickrPhotosViewController+UICollectionViewDataSource.swift and add the following method:

override func collectionView(
  _ collectionView: UICollectionView,
  viewForSupplementaryElementOfKind kind: String,
  at indexPath: IndexPath
) -> UICollectionReusableView {
  switch kind {
  // 1
  case UICollectionView.elementKindSectionHeader:
    // 2
    let headerView = collectionView.dequeueReusableSupplementaryView(
      ofKind: kind,
      withReuseIdentifier: "\(FlickrPhotoHeaderView.self)",
      for: indexPath)

    // 3
    guard let typedHeaderView = headerView as? FlickrPhotoHeaderView
    else { return headerView }

    // 4
    let searchTerm = searches[indexPath.section].searchTerm
    typedHeaderView.titleLabel.text = searchTerm
    return typedHeaderView
  default:
    // 5
    assert(false, "Invalid element type")
  }
}

This method returns a view for a given kind of supplementary view. For this app, you only have headers, but you can also have other kinds of supplementary views.

Here’s the breakdown of this method:

  1. Handle the UICollectionView.elementKindSectionHeader kind which UICollectionViewFlowLayout supplies for you. By checking the header box in an earlier step, you told the flow layout to begin supplying headers. If you weren’t using the flow layout, you wouldn’t get this behavior for free.
  2. Dequeue the header using the storyboard identifier.
  3. Check that the view is the right type and, if not, simply return it.
  4. Set the search term as the text on the view’s label.
  5. Assert in the case that the kind was not what was expected.

This is a good place to build and run. You’ll get a nice section header for each search, and the layout adapts nicely in rotation scenarios on all device types:

Search results showing food photos

Bravo! It’s a small change, but the app looks so much better now, doesn’t it? :]

But there’s more work to do!

Interacting With Cells

In this section, you’ll learn some ways to interact with cells including selecting single and multiple cells and using drag-and-drop to reorder cells.

First up is learning how to respond to selecting a cell. You’ll make the UICollectionView animate a layout change when the user selects an item.

Open FlickrPhotosViewController.swift and add the following computed property to the top of FlickrPhotosViewController:

// 1
var largePhotoIndexPath: IndexPath? {
  didSet {
    // 2
    var indexPaths: [IndexPath] = []
    if let largePhotoIndexPath = largePhotoIndexPath {
      indexPaths.append(largePhotoIndexPath)
    }

    if let oldValue = oldValue {
      indexPaths.append(oldValue)
    }

    // 3
    collectionView.performBatchUpdates({
      self.collectionView.reloadItems(at: indexPaths)
    }, completion: { _ in
      // 4
      if let largePhotoIndexPath = self.largePhotoIndexPath {
        self.collectionView.scrollToItem(
          at: largePhotoIndexPath,
          at: .centeredVertically,
          animated: true)
      }
    })
  }
}

Here’s what is going on in the code above:

  1. largePhotoIndexPath holds the index path of the currently selected photo item. It’s an optional because there may not be a selection.
  2. When this property changes, you must also update the collection view. didSet is an easy way to manage this. You might have two cells that need to be reloaded if the user had previously selected a different cell or tapped the same cell a second time to deselect it.
  3. performBatchUpdates(_:completion:) animates changes to the collection view.
  4. Once the animation completes, scroll the selected cell to the middle of the screen.

Next, click File ▸ New ▸ File then select Swift File and click Next. Name the file FlickrPhotosViewController+UICollectionViewDelegate and click Create. Then replace the contents of the file with the following:

import UIKit

extension FlickrPhotosViewController {
  override func collectionView(
    _ collectionView: UICollectionView, 
    shouldSelectItemAt indexPath: IndexPath
  ) -> Bool {
    // 1
    if largePhotoIndexPath == indexPath {
      largePhotoIndexPath = nil
    } else {
      largePhotoIndexPath = indexPath
    }

    // 2
    return false
  }
}

This method is pretty simple. It implements one of the UICollectionViewDelegate methods which is called when an item is selected. Here’s what the method does:

  1. If the IndexPath of the cell the user tapped is already selected, set largePhotoIndexPath to nil to indicate deselection. Otherwise, set it to the current value of indexPath. This will fire the didSet property observer you just implemented.
  2. Return false so that the collection view doesn’t set its own flag on which cell is selected. You handle selection yourself and you don’t want the collection view interfering.

Now, open FlickrPhotosViewController+UICollectionViewDelegateFlowLayout.swift and find collectionView(_:layout:sizeForItemAt:). Add the following code to the beginning of the method:

if indexPath == largePhotoIndexPath {
  let flickrPhoto = photo(for: indexPath)
  var size = collectionView.bounds.size
  size.height -= (FlickrConstants.sectionInsets.top + 
    FlickrConstants.sectionInsets.right)
  size.width -= (FlickrConstants.sectionInsets.left + 
    FlickrConstants.sectionInsets.right)
  return flickrPhoto.sizeToFillWidth(of: size)
}

This makes the size of the currently selected cell fill the width of the collection view while maintaining the image’s aspect ratio.

Build and run. Make a search and tap one of the images.

UICollectionViewCell selected state showing bowl of noodles

You did it! :]

Now, because you’re increasing the size of the cell, you need a larger image to make it look good. Your next task is to download the larger image upon request.

Providing Selection Feedback

You’ll want to show a spinner while the larger image downloads. Do that first.

Open Main.storyboard and open you the object library with Command-Shift-L. Drag an activity indicator onto the image view in the collection view cell.

Open the Attributes inspector, set the style to Large and check Hides When Stopped.

Using the layout guides, drag the indicator to the center of the ImageView, then use the Align menu to set constraints to center the indicator horizontally and vertically.

Activity Indicator View dialog

Open FlickrPhotoCell.swift in an additional editor pane and then Control-drag from the activity indicator onto FlickrPhotoCell. Name the outlet activityIndicator. It will add the following code to the class:

@IBOutlet weak var activityIndicator: UIActivityIndicatorView!

While in FlickrPhotoCell.swift, add the following methods:

override var isSelected: Bool {
  didSet {
    imageView.layer.borderWidth = isSelected ? 10 : 0
  }
}

override func awakeFromNib() {
  super.awakeFromNib()
  imageView.layer.borderColor = themeColor.cgColor
  isSelected = false
}

This simply changes the border of the image whether the cell is selected or not.

It’s time to download that large image!

Loading the Large Image

Now, open FlickrPhotosViewController+Helper.swift and add a convenience method in the extension to download the large version of a Flickr image:

func performLargeImageFetch(
  for indexPath: IndexPath, 
  flickrPhoto: FlickrPhoto,
  cell: FlickrPhotoCell
) {
  // 1
  cell.activityIndicator.startAnimating()

  // 2
  flickrPhoto.loadLargeImage { [weak self] result in
    cell.activityIndicator.stopAnimating()

    // 3
    guard let self = self else {
      return
    }

    switch result {
    // 4
    case .success(let photo):
      if indexPath == self.largePhotoIndexPath {
        cell.imageView.image = photo.largeImage
      }
    case .failure:
      return
    }
  }
}

Here’s a step-by-step explanation of the code above:

  1. Start the activity indicator to show that something is happening.
  2. Use the convenience method on FlickrPhoto to start downloading the large image. Once that load completes, you need to stop the activity indicator, which will also hide it.
  3. Because you’re in a closure that captures self weakly, ensure the view controller still exists.
  4. If successful and if the indexPath matches the current largePhotoIndexPath, set the imageView of the cell to largeImage. If the image download failed, simply do nothing.

Finally, in FlickrPhotosViewController+UICollectionViewDataSource.swift, replace collectionView(_:cellForItemAt:) with the following:

override func collectionView(
  _ collectionView: UICollectionView, 
  cellForItemAt indexPath: IndexPath
) -> UICollectionViewCell {
  guard let cell = collectionView.dequeueReusableCell(
    withReuseIdentifier: FlickrConstants.reuseIdentifier, 
    for: indexPath) as? FlickrPhotoCell
  else {
    preconditionFailure("Invalid cell type")
  }

  let flickrPhoto = photo(for: indexPath)

  // 1
  cell.activityIndicator.stopAnimating()

  // 2
  guard indexPath == largePhotoIndexPath else {
    cell.imageView.image = flickrPhoto.thumbnail
    return cell
  }

  // 3
  cell.isSelected = true
  guard flickrPhoto.largeImage == nil else {
    cell.imageView.image = flickrPhoto.largeImage
    return cell
  }

  // 4
  cell.imageView.image = flickrPhoto.thumbnail

  // 5
  performLargeImageFetch(for: indexPath, flickrPhoto: flickrPhoto, cell: cell)

  return cell
}

Here’s an explanation of the code above:

  1. Stop the activity indicator in case it was currently active. This cell might be reused from a cell that was loading an image and had the activity spinner active.
  2. Check if the cell is for a non-selected index path. If largePhotoIndexPath doesn’t match the indexPath of the current cell, set the image to the thumbnail version and return.
  3. At this point, you can be sure of having a non-empty largePhotoIndexPath value that is also the user selected index path. So, you check if the large image is already downloaded for the selected index and then set the image to the large image and return.
  4. Finally, the case when largePhotoIndexPath is the same as the selected index path and the largeImage is not already loaded. You set the thumbnail first.
  5. Then, call the convenience method you created above to fetch the large image version and return the cell.

Now’s a great time to build and run to check your work. Perform a search, then select an image cell. You can see it scales up and animates to the center of the screen. Tapping it again animates it back to its original state.

Highlighted, large cell

Excellent work! Next, you’ll implement multiple selections and sharing.

Selecting Multiple Cells

You’ll add a feature where you can select multiple images and then share them. You’ll collect selected images in an array and then use a native share sheet to allow sharing out via any available method on the device.

Open FlickrPhotosViewController.swift and add the following at the top of FlickrPhotosViewController.

var selectedPhotos: [FlickrPhoto] = []
let shareTextLabel = UILabel()
var isSharing = false

Here’s what each of these is for:

  • selectedPhotos: keeps track of all currently selected photos while in sharing mode.
  • shareTextLabel: gives the user feedback about how many photos are currently selected.
  • isSharing: used to track whether the view controller is in sharing mode.

Next, open FlickrPhotosViewController+Helper.swift and add the following method to the extension:

func updateSharedPhotoCountLabel() {
  if isSharing {
    shareTextLabel.text = "\(selectedPhotos.count) photos selected"
  } else {
    shareTextLabel.text = ""
  }

  shareTextLabel.textColor = themeColor

  UIView.animate(withDuration: 0.3) {
    self.shareTextLabel.sizeToFit()
  }
}

You’ll call this method to keep shareTextLabel up to date. This method checks isSharing. If the app is currently in sharing mode, it sets the label text and animate the size change to fit along with all the other elements in the navigation bar.

That handles the state behind sharing. Now, you need to add a way for the user to enter sharing mode.

Keeping Track of Sharing

Open FlickrPhotosViewController.swift again and replace the isSharing stored property with the following computed property below largePhotoIndexPath:

var isSharing = false {
  didSet {
    // 1
    collectionView.allowsMultipleSelection = isSharing

    // 2
    collectionView.selectItem(at: nil, animated: true, scrollPosition: [])
    selectedPhotos.removeAll()

    guard let shareButton = navigationItem.rightBarButtonItems?.first else {
      return
    }

    // 3
    guard isSharing else {
      navigationItem.setRightBarButtonItems([shareButton], animated: true)
      return
    }

    // 4
    if largePhotoIndexPath != nil {
      largePhotoIndexPath = nil
    }

    // 5
    updateSharedPhotoCountLabel()

    // 6
    let sharingItem = UIBarButtonItem(customView: shareTextLabel)
    let items: [UIBarButtonItem] = [
      shareButton,
      sharingItem
    ]

    navigationItem.setRightBarButtonItems(items, animated: true)
  }
}

After the property is set, here’s what happens:

  1. Set the allowsMultipleSelection property of the collection view. If you’re in sharing mode, then you need to be able to select multiple cells.
  2. Deselect all cells, scroll to the top and remove any existing items from the array of selected photos.
  3. If sharing is not enabled, set the share button to the default state and return.
  4. Make sure largePhotoIndexPath is set to nil as you don’t want any previously selected cell to still be shown large.
  5. Call the convenience method you created above to update the share label.
  6. Update the bar button items to show the label alongside the share button.

Adding a Share Button

You now need to add the share button to the UI. First open Main.storyboard, open the object library with Command-Shift-L and drag a Bar Button Item onto the FlickrPhotosViewController to the right of the search bar in the navigation bar. Open the Attributes inspector and set the System Item to Action.

Then open FlickrPhotosViewController.swift in an additional editor pane, and Control-drag from the share button to the bottom of the FlickrPhotosViewController class. Create an Action named shareButtonTapped and set the Type to UIBarButtonItem.

Bar button action outlet

Fill in the method as follows:

// 1
guard !searches.isEmpty else {
  return
}

// 2
guard !selectedPhotos.isEmpty else {
  isSharing.toggle()
  return
}

// 3
guard isSharing else {
  return
}

// TODO: Add photo sharing logic!

This method:

  1. Checks if there are any searches. If not, then there are no photos to share so simply return.
  2. If there are no selected photos currently then toggle sharing on or off.
  3. Simply return if in sharing mode.

Now open FlickrPhotosViewController+UICollectionViewDelegate.swift and add the following to the top of collectionView(_:shouldSelectItemAt:):

guard !isSharing else {
  return true
}

This method is used to handle single selection when not in sharing mode. So, if the app is in sharing mode, you want the method to do nothing.

Next, add the following method below collectionView(_:shouldSelectItemAt:):

override func collectionView(
  _ collectionView: UICollectionView, 
  didSelectItemAt indexPath: IndexPath
) {
  guard isSharing else {
    return
  }

  let flickrPhoto = photo(for: indexPath)
  selectedPhotos.append(flickrPhoto)
  updateSharedPhotoCountLabel()
}

This responds to an item being selected in the collection view but is called after the item is selected. Here you add the selected photo to the sharedPhotos array and update the shareTextLabel text.

Next, add the following underneath the method you just added:

override func collectionView(
  _ collectionView: UICollectionView,
  didDeselectItemAt indexPath: IndexPath
) {
  guard isSharing else {
    return
  }

  let flickrPhoto = photo(for: indexPath)
  if let index = selectedPhotos.firstIndex(of: flickrPhoto) {
    selectedPhotos.remove(at: index)
    updateSharedPhotoCountLabel()
  }
}

This removes an item from selectedPhotos and updates the label if the user taps a selected cell to deselect it.

Finally, open FlickrPhotosViewController again and find the // TODO comment in shareButtonTapped(_:) and replace it with this:

// 1
let images: [UIImage] = selectedPhotos.compactMap { photo in
  guard let thumbnail = photo.thumbnail else {
    return nil
  }

  return thumbnail
}

// 2
guard !images.isEmpty else {
  return
}

// 3
let shareController = UIActivityViewController(
  activityItems: images,
  applicationActivities: nil)

// 4
shareController.completionWithItemsHandler = { _, _, _, _ in
  self.isSharing = false
  self.selectedPhotos.removeAll()
  self.updateSharedPhotoCountLabel()
}

// 5
shareController.popoverPresentationController?.barButtonItem = sender
shareController.popoverPresentationController?.permittedArrowDirections = .any
present(shareController, animated: true, completion: nil)

Here’s what that does:

  1. Create an array of images containing thumbnails of photos selected by the user.
  2. Ensure the selected images array is not empty.
  3. Create a native UIActivityViewController and pass the images array in the activityItems parameter.
  4. Define the completion handler to update properties after the user performs a share action.
  5. Present the activity view controller over the current view controller. iOS presents any system apps or services that can handle a list of images.

Once again, check your work! Build and run, perform a search and tap the share button in the navigation bar. Select multiple images and watch the label update as you select new cells:

Multiple cells selected

Tap the share button again, and the native share sheet will appear. If you’re on a device, you can select any app or service that accepts images to share:

Share action box

Congratulations! :]

Reordering Cells

In this last segment, you’ll learn how to use the native drag-and-drop feature to reorder cells in the collection view. iOS has some nice convenience methods built-in for UICollectionView, which take advantage of the drag-and-drop features that iOS 11 added.

To use drag and drop, you have to be aware of two protocols. UICollectionViewDragDelegate drives drag interactions and UICollectionViewDropDelegate drives drop interactions. You’ll implement the drag delegate first, then test behavior and finally complete this feature with a drop delegate.

Implementing Drag Interactions

Click File ▸ New ▸ File and select Swift File. Click Next, name it FlickrPhotosViewController+DragAndDropDelegate and click Create.

Replace the contents of the file with the following:

import UIKit

// MARK: - UICollectionViewDragDelegate
extension FlickrPhotosViewController: UICollectionViewDragDelegate {
  func collectionView(
    _ collectionView: UICollectionView,
    itemsForBeginning session: UIDragSession,
    at indexPath: IndexPath
  ) -> [UIDragItem] {
    // 1
    let flickrPhoto = photo(for: indexPath)
    guard let thumbnail = flickrPhoto.thumbnail else {
      return []
    }

    // 2
    let item = NSItemProvider(object: thumbnail)

    // 3
    let dragItem = UIDragItem(itemProvider: item)

    // 4
    return [dragItem]
  }
}

collectionView(_:itemsForBeginning:at:) is the only required method for this protocol, and it’s also the only one you need for this feature. Here’s what it does:

  1. First, you fetch the thumbnail of the selected photo from the photo object.
  2. Create an NSItemProvider object referencing the thumbnail. This is used by the drag system to indicate what item is being dragged.
  3. Then, create a UIDragItem that represents the selected item to be dragged.
  4. The method must return an array of UIDragItem objects, so you add the dragItem created above in an array.

Next, you need to let the collection view know it can handle drag interactions. Open FlickrPhotosViewController.swift and implement viewDidLoad() above shareButtonTapped(_:):

override func viewDidLoad() {
  super.viewDidLoad()
  collectionView.dragInteractionEnabled = true
  collectionView.dragDelegate = self
}

In this method, you set the collectionView’s dragInteractionEnabled property to true and set the dragDelegate.

Now’s a great time to build and run. Perform a search, and now you’ll be able to long-press a cell to see the drag interaction. You can see it lift and can drag it around. But, you won’t be able to drop it anywhere just yet.

Photo being dragged

Next up, it’s time to implement drop behavior.

Implementing Drop Interactions

Open FlickrPhotosViewController+DragAndDropDelegate.swift. Now, you need to implement some methods from UICollectionViewDropDelegate to enable the collection view to accept dropped items from a drag session. This will also allow you to reorder the cells by taking advantage of the provided index paths from the drop methods.

Create another extension to conform to UICollectionViewDropDelegate:

extension FlickrPhotosViewController: UICollectionViewDropDelegate {
  func collectionView(
    _ collectionView: UICollectionView, 
    canHandle session: UIDropSession
  ) -> Bool {
    return true
  }
}

Typically in this method, you would inspect the proposed drop items and decide whether you wanted to accept them. Because you’re enabling drag-and-drop for only one item type in this app, you simply return true here. Don’t worry about the compiler error; you’ll fix it shortly.

To update the source data array with changes, you need to make it mutable. Open FlickrSearchResults.swift and update searchResults as follows:

var searchResults: [FlickrPhoto]

Before you implement the next method, open FlickrPhotosViewController+Helper.swift and place these methods right under photo(for:):

func removePhoto(at indexPath: IndexPath) {
  searches[indexPath.section].searchResults.remove(at: indexPath.row)
}

func insertPhoto(_ flickrPhoto: FlickrPhoto, at indexPath: IndexPath) {
  searches[indexPath.section].searchResults.insert(
    flickrPhoto, 
    at: indexPath.row)
}

These methods allow removing and inserting photos into the searchResults array.

Making the Drop

Open FlickrPhotosViewController+DragAndDropDelegate.swift and implement the following method inside the UICollectionViewDropDelegate extension:

func collectionView(
  _ collectionView: UICollectionView,
  performDropWith coordinator: UICollectionViewDropCoordinator
) {
  // 1
  guard let destinationIndexPath = coordinator.destinationIndexPath else {
    return
  }

  // 2
  coordinator.items.forEach { dropItem in
    guard let sourceIndexPath = dropItem.sourceIndexPath else {
      return
    }

    // 3
    collectionView.performBatchUpdates({
      let image = photo(for: sourceIndexPath)
      removePhoto(at: sourceIndexPath)
      insertPhoto(image, at: destinationIndexPath)
      collectionView.deleteItems(at: [sourceIndexPath])
      collectionView.insertItems(at: [destinationIndexPath])
    }, completion: { _ in
      // 4
      coordinator.drop(
        dropItem.dragItem,
        toItemAt: destinationIndexPath)
    })
  }
}

This delegate method accepts the drop items and performs maintenance on the collection view and the underlying data storage array to properly reorder the dropped items. You’ll see UICollectionViewDropCoordinator referred to here. This UIKit class gives you more information about the proposed drop items.

Here’s what happens, in detail:

  1. Get destinationIndexPath from the drop coordinator.
  2. Loop through the items — an array of UICollectionViewDropItem objects — and ensure each has a sourceIndexPath.
  3. Perform batch updates on the collection view, removing items from the array at the source index and inserting them in the destination index. After completing that, perform deletes and updates on the collection view cells.
  4. In the completion handler, perform the drop action.

Open FlickrPhotosViewController.swift, find viewDidLoad() and add the following code to the end of the method:

collectionView.dropDelegate = self

You simply set the dropDelegate.

You’re doing great! Just the last step to go now! Open FlickrPhotosViewController+DragAndDropDelegate.swift and implement the following method at the bottom of the UICollectionViewDropDelegate extension:

func collectionView(
  _ collectionView: UICollectionView,
  dropSessionDidUpdate session: UIDropSession,
  withDestinationIndexPath destinationIndexPath: IndexPath?
) -> UICollectionViewDropProposal {
  return UICollectionViewDropProposal(
    operation: .move,
    intent: .insertAtDestinationIndexPath)
}

The UIKit drop session calls this method constantly during interaction to interpolate where the user is dragging the items and to give other objects a chance to react to the session.

This delegate method causes the collection view to respond to the drop session with UICollectionViewDropProposal that indicates the drop will move items and insert them at the destination index path.

Great! It’s time to build and run.

Drop action

You now see a drag session coupled with some nice behavior in your collection view. As you drag your item over the screen, other items shift to move out of the way, indicating the drop can be accepted at that position. You can even reorder items between different search sections!

Where to Go From Here?

Congratulations! You’ve just finished a whirlwind tour of some pretty advanced UICollectionView features. :]

Along the way, you learned how to delineate sections with reusable headers, select cells, update cell layout, perform multiple selections, reorder items and much more. You can download the final project by clicking the Download Materials button at the top or bottom of this tutorial.

You can also find an interesting video course about UICollectionView at Collection Views.

If you have any questions or comments about UICollectionView or this tutorial, please join the forum discussion below.

Average Rating

5/5

Add a rating for this content

5 ratings

More like this

Contributors

Comments