UICollectionView Tutorial: Headers, Selection and Reordering

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

4.7 (7) · 1 Review

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.