UITableView Swipe to Delete Workflow in Swift

Data management applications, by which I mean an app where you’re allowing users to add, edit, and delete bits of data as part of your app’s core function, very likely use a table view (or two) to visualize lists of information that users of the app can interact with.

Making changes to the information listed in the table view and signaling those changes in a fluent way becomes a top concern for these types of apps. How do we allow users to add or remove “records” to the system? Furthermore, how do we signal that those changes were effective and refresh the view of the data in the UI?

The primary concern I want to focus in on in this article is the ever-common paradigm of “swipe to delete” when using a table view. What could the workflow of deleting a row from the table view (and its data source) look like? How could it be implemented in Swift? Let’s explore…

Workflow

The workflow of “swipe to delete” that I typically use in my own applications is this:

  • Swipe a table view row and have the Delete button appear.
  • Press the delete button, which triggers a confirmation: “Do you really want to delete this?”
  • Based on the user’s response to the confirmation, delete the object from the data source and remove it from the table view, or cancel.

The confirmation step is atypical for iOS it seems. Swiping to delete an e-mail or a reminder or just about anything else in Apple’s own apps simply deletes the item right away.

There may be a good reason for this, but I like to give folks an out if they didn’t mean to do it. It’s fair enough to say, “Well, they went to the effort of swiping the row and pressing the button… surely they mean to do it!”. I still feel more comfortable if I get the opportunity to cancel something like a delete operation. Feel free to disagree there – for this article, I’ll assume you want to include that into your delete workflow, and will demonstrate a simple way to implement all three steps of the strategy.

Implementation

To demonstrate the implementation of this workflow, suppose that we’ve got a table view listing out all of the planets in our solar system (all of the news on Pluto this week has me thinking in this direction, so I just went with it). Feel free to grab the example project over at GitHub:

The table view looks like this once its data source is all configured:

Table View with Planets

Swipe

What we want is to be able to swipe one of the rows and delete the planet from the data source and, consequently, remove it from the table view in a nice, fluid way:

Table View - Delete Button Revealed

To accomplish this, we need to do a couple of things.

First, your view controller needs to conform to the UITableViewDelegate protocol, because the swipe to delete functionality is encapsulated by one of the table view delegate methods. It’s a simple change at the class declaration:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate { ... }

The delegate method that causes the buttons to appear on swipe is called tableView(_:commitEditingStyle:forRowAtIndexPath

Its implementation for the example I’m working through looks like this:

 1class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 2
 3    // ...
 4
 5    var deletePlanetIndexPath: NSIndexPath? = nil
 6
 7    // ...
 8
 9    func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) {
10        if editingStyle == .Delete {
11            deletePlanetIndexPath = indexPath
12            let planetToDelete = planets[indexPath.row]
13            confirmDelete(planetToDelete)
14        }
15    }
16
17    // ...
18}

So what’s happening here? The primary thing to notice is that we’re evaluating the editingStyle from the method’s parameter list. Comparing it to the .Delete UITableViewCellEditingStyle value is what allows us to know that the Delete button was tapped.

Since I have a confirmation step to take care of, I’ve chosen to store the indexPath of the row we’re wanting to delete in a class-viewable variable so that I can use it later on when we handle the deletion (if the user confirms it).

The final step of this function is to call confirmDelete(), which has its own explanation coming up…

Confirm

The next step is confirming that the user really wants to delete the particular planet they’ve initiated the delete action on:

Delete Confirmation

How is this implemented? It all boils down to using iOS 8’s new UIAlertController:

 1class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 2
 3    // ...
 4
 5    func confirmDelete(planet: String) {
 6        let alert = UIAlertController(title: "Delete Planet", message: "Are you sure you want to permanently delete \(planet)?", preferredStyle: .ActionSheet)
 7        
 8        let DeleteAction = UIAlertAction(title: "Delete", style: .Destructive, handler: handleDeletePlanet)
 9        let CancelAction = UIAlertAction(title: "Cancel", style: .Cancel, handler: cancelDeletePlanet)
10        
11        alert.addAction(DeleteAction)
12        alert.addAction(CancelAction)
13        
14        // Support display in iPad
15        alert.popoverPresentationController?.sourceView = self.view
16        alert.popoverPresentationController?.sourceRect = CGRectMake(self.view.bounds.size.width / 2.0, self.view.bounds.size.height / 2.0, 1.0, 1.0)
17        
18        self.presentViewController(alert, animated: true, completion: nil)
19    }
20
21    // ...
22}

We’re asking the user if they’re sure they want to permanently delete the planet using the .ActionSheet style.

The next step of the function is to create a couple of UIAlertAction buttons: one for Delete and one for Cancel, with the appropriate style (.Destructive and .Cancel, respectively).

Finally, we provide a function to each UIAlertAction instance. The handlers, of course, could have been implemented with closures, but I chose to built out a couple of named functions, just to be able to step through it with you, and to separate out the logic of the steps a little more. Feel free to do whichever seems most natural to you.

Delete or cancel

This is the final step! The following code snippet is an implementation of the two handlers specified for the initialization step of the UIAlertAction buttons, which were handleDeletePlanet and cancelDeletePlanet:

 1class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {
 2
 3    // ...
 4
 5    var deletePlanetIndexPath: NSIndexPath? = nil
 6
 7    // ...
 8
 9    func handleDeletePlanet(alertAction: UIAlertAction!) -> Void {
10        if let indexPath = deletePlanetIndexPath {
11            tableView.beginUpdates()
12            
13            planets.removeAtIndex(indexPath.row)
14            
15            // Note that indexPath is wrapped in an array:  [indexPath]
16            tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
17            
18            deletePlanetIndexPath = nil
19            
20            tableView.endUpdates()
21        }
22    }
23    
24    func cancelDeletePlanet(alertAction: UIAlertAction!) {
25        deletePlanetIndexPath = nil
26    }
27
28    // ...
29}

Clearly, handleDeletePlanet() is the most involved. The essential process is this:

  • Call beginUpdates() on the table view instance to signal the start of UI updates to the table view.
  • Remove the planet from the data source using the deletePlanetIndexPath we set in the alert controller step.
  • Call deleteRowsAtIndexPaths() on the table view to remove the planet from the UI.
  • Reset the class-viewable deletePlanetIndexPath to nil.
  • Call endUpdates() on the table view instance to complete the UI updates.

There’s one gotcha, that I’ve tried to highlight by way of comment in the code snippet: Notice that within the call to deleteRowsAtIndexPaths (plural), I’ve wrapped the indexPath we’re removing in an array. It’s subtle, but this delegate method expects an array of indexPath instances, not a single index path instance. It’s simple enough, but it can catch you off guard if you’re in the mindset of removing a single row and come across this method, which is flexible enough to allow you to delete several at a time.

Wrapping up

Swipe -> confirm -> delete or cancel. Those are the steps we analyzed in this commonly needed workflow in data management applications.

comments powered by Disqus