Allowing users to delete cells

Any good contacts app enables users to remove contacts. The previous version of Hello-Contacts used a swipe gesture on a table view cell to delete a cell. Swiping a cell made a button appear that the user could tap and the corresponding UITableViewDelegate method was called.

Unfortunately, UICollectionViewDelegate does not specify a similar delegate method for deleting cells. This means that you'll need to do a little more work to implement cell deletion for a collection view. A very simple implementation would be to have the user long-press on a cell, ask them whether they want to delete the contact, and reload the collection view if needed. This would work but the deleted contact would quickly disappear, and the whole thing would look quite choppy.

Luckily, UICollectionView defines several methods that you can use to update the collection view's contents in a very nice way. For instance, when you delete a cell from the collection view, you can have the remaining cells animate to their new positions. This looks way better than simply reloading the entire collection view without an animation. Good iOS developers will always make sure that they go the extra mile to find a nice, smooth way to transition between interface states. So animating cells when deleting one is a great thing to have.

If you look at Apple's documentation on UICollectionView, you'll find that there is a lot of information available about collection views. If you scroll all the way down to the Symbols section, there is a subsection named Inserting, Moving and Deleting items. Perfect, this is precisely the kind of information you need to implement cell deletion. More specifically, the deleteItems(_:) method looks like it's exactly what you need to get the job done.

The requirements for the cell deletion feature are the following:

  1. The user long-presses on a cell.
  2. An action sheet appears to verify whether the user wants to delete this cell.
  3. If the user confirms the deletion, the cell is removed, and the layout animates to its new state. The contact is also removed from the contacts array.
To detect certain user interactions such as double-tapping, swiping, pinching, and long-pressing, you make use of gesture recognizers. A gesture recognizer is a special object provided by UIKit that can detect certain gestures. When such a gesture occurs, it calls a selector (method) on a target object. This method can then perform a specific task in response to the gesture.

To keep things simple, you should add a gesture recognizer to the collection view as a whole. Adding the gesture recognizer to the whole collection view rather than its cells is a lot simpler because you can reuse a single recognizer, and figuring out information about the tapped cell and responding to the long-press is easier to do from the view controller than the tapped cell itself.

In a moment, you'll see how to find the tapped cell in the collection view. First, set up the recognizer by adding the following lines to viewDidLoad() in ViewController.swift:

let longPressRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.userDidLongPress(_:)))
collectionView.addGestureRecognizer(longPressRecognizer)

The first line sets up the long-press gesture recognizer. The target given to the gesture recognizer is self. This means that the current instance of ViewController is used to call the action on. The action is the second argument for the gesture recognizer. The selector passed to it refers to the method that is called when the user performs the gesture.

The second line adds the gesture recognizer to the collection view. This means that the gesture recognizer will only detect long-presses that occur within the collection view. When a long-press occurs on the collection view, the gesture recognizer will inform the ViewController of this event by calling userDidLongPress(_:) on it.

Now that your gesture recognizer is set up and you know a bit about how it works, add the following implementation of userDidLongPress(_:) to ViewController.swift:

@objc func userDidLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
  // 1
  let tappedPoint = gestureRecognizer.location(in: collectionView)
  guard let indexPath = collectionView.indexPathForItem(at: tappedPoint),
    let tappedCell = collectionView.cellForItem(at: indexPath)
    else { return }

  // 2
  let confirmationDialog = UIAlertController(title: "Delete contact?", message: "Are you sure you want to delete this contact?", preferredStyle: .actionSheet)

  let deleteAction = UIAlertAction(title: "Yes", style: .destructive, handler: { [weak self] _ in
    // 3
    self?.contacts.remove(at: indexPath.row)
    self?.collectionView.deleteItems(at: [indexPath])
  })

  let cancelAction = UIAlertAction(title: "No", style: .default, handler: nil)

  confirmationDialog.addAction(deleteAction)
  confirmationDialog.addAction(cancelAction)

  // 4
  if let popOver = confirmationDialog.popoverPresentationController {
    popOver.sourceView = tappedCell
  }

  present(confirmationDialog, animated: true, completion: nil)
}

Note that this method is prefixed with @objc. This is required because selectors are a dynamic feature that originated in Objective-C. The userDidLongPress(_:) method must be exposed to the Objective-C runtime by prefixing it with @objc.

The first step in the implementation of this method is to obtain some information about the cell that was tapped. By taking the location at which the long-press occurred inside of the collection view, it is possible to determine the index path that corresponds with this long press. The index path can then be used to obtain a reference to the cell that the user was long-pressing on. Note that the user can also long-press outside of a cell. If this happens, the guard will cause the method to return early because there will be no index path that corresponds to the long-press location. If everything is fine, the code continues to the second step in this method.

To display an action sheet to the user, an instance of UIAlertController is created. You have already seen this object in action in the previous chapter when it was used to show an alert when the user tapped on a table view cell. The main difference between the alert implementation and this implementation is preferredStyle. Since this alert should show as an action sheet, the .actionSheet style is passed as the preferred style. This style will make an action sheet pop up from the bottom of the screen.

In the delete action for this action sheet, the contact is removed from the contacts array that is used as a data source. After updating the data source, the cell is removed from the collection view using deleteItems(at:). When you update the items in a collection view, it is imperative to make sure that you update the data source first. If you don't do this, the app is likely to crash due to internal inconsistency exceptions. To see this crash occur, reverse the order of commands in the delete action.

Whenever you update a collection view in a way that moves or deletes its contacts, always make sure to update the underlying data source first. Not doing this will crash your app with an internal inconsistency error.

The fourth and last step before presenting the action sheet is some defensive programming. Larger screens, such as an iPad screen, display action sheets as popover views instead of action sheets. You can detect whether an action sheet will be shown as a popover by checking the popoverPresentationController object on the alert controller instance you have created. If a popoverPresentationController object exists, the action sheet will be presented as a popover and requires a sourceView object to be set. Not setting the sourceView object on a popover crashes your app, so it's better to provide a sourceView object than have your app crash when something unexpected happens.

When you display an action sheet, make sure to check whether a popoverPresentationController object exists and if it does, make sure to set a sourceView or sourceRect object. Devices with larger screens present action sheets as popovers and not setting a source for the popover crashes your app.

This wraps up implementing cell deletion for the collection view. You can try long-pressing on a contact cell now to see your action sheet appear. Deleting a contact should nicely animate the update. Even though it took a little bit more effort to implement cell deletion for collection view than it did for table view, it wasn't too bad. Next up is cell reordering.

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

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