Skip to content

Updating UITableView Swipe Actions for iOS 11+

Back in iOS 11 Apple changed the way swipe actions should be implemented on TableViews; specifically they deprecated:

func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?

There is still a fair chance that this is the method you’d end up implementing if you googled it or looked on StackOverflow, as the bulk of the information around seems to refer to the pre-iOS 11 approach.

Until now I’ve not addressed this within my various codebases either, but as I now need to implement it for a new iOS 13 app it seemed like a good time to retrofit it to some of the older code too. Writing it up on here will help fix the new pattern in my mind, and might even help someone else avoid using an outdated API in their code.

One advantage of adopting the current implementation is that both left and right, or more correctly leading and trailing, swipes are now supported ‘out of the box’. The two ‘new’ functions within the Tableview’s delegate protocol that facilitate the swipe actions are:

optional func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?

optional func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration?

Both of these functions return optional instances of UISwipeActionsConfiguration, and, as you might expect, returning nil displays the default set of swipe actions for the Tableview row. What is slightly less obvious is that the this return type wraps, and is initialised with, an array of UIContextualAction classes. These are the meat of the solution and it’s at this lower level I’ll mainly focus.

For this example I’m going to focus on the trailing actions as they tend to be more common, but the same approach can be applied to the leading actions to handle a swipe from the leading edge. I’ll be creating two actions to demonstrate slightly different approaches. I’m also going to use a constructor method to create the UIContextualActions to keep actual delegate method cleaner. This means that the actual delegate method simply looks like this:

func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
          [makeArchiveContextualAction(forRowAt: indexpath),
           makeDeleteContextualAction(forRowAt: indexPath)])

Note, that as this is a single line method, I’m adopting the Swift 5 pattern of omitting the return keyword.

So that’s the easy bit done 😀. How about these contextual actions then? They’re also reasonably straightforward, but are somewhat different to the old approach in that they wrap their appearance attributes and a closure that contains the resulting action together.

func makeArchiveContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction {
   let action = UIContextualAction(style: .normal,
                                   title: "Archive",
                                   handler: { (contextualAction: UIContextualAction, swipeButton: UIView, completionHandler: (Bool) -> Void) in
         if self.notifications[indexPath.row].protected == false {
            self.tableView.cellForRow(at: indexPath)?.accessoryType   .checkmark
         } else {
      action.backgroundColor = .magenta
      return action
   func makeDeleteContextualAction(forRowAt indexPath: IndexPath) -> UIContextualAction {
      UIContextualAction(style: .destructive,
          title: "Delete") { (contextualAction: UIContextualAction, swipeButton: UIView, completionHandler: (Bool) -> Void) in
          self.notifications.remove(at: indexPath.row)
          if self.notifications.count > 0 {
             self.tableView.deleteRows(at: [indexPath], with: .automatic)
          } else {
             self.tableView.reloadRows(at: [indexPath], with: .automatic)
          self.updateHeaderNumber(to: self.notifications.count)

I’ve implemented the two actions slightly differently, just to illustrate possible approaches. The Archive action has been written out in full, creating an action property with the closure as a parameter, and then this action is explicitly returned. This approach has allowed the .backgroundColor attribute to be set, and can also be used to set the .image property, so a glyph can be used instead of the title text.

For the delete action I’ve again taken advantage of Swift 5’s implicit return for single-line functions (although for a statement this long it’s questionable whether this is a good idea, as implicit return is less obvious) and I’ve used the trailing closure syntax to reduce the number of nested levels of brackets/parentheses.

The key points to note are:

  • the action has a choice of .destructive or .normal styles. They main (only?) difference being the background colour of the swipe button: with the former it’s and for the latter
  • the bulk of the work is performed in the closure. This is where you implement the actions for the swipe buttons, either directly of by calling out to another method.
  • one of the closure’s inputs is a completion handler; this is supplied by the operating system at call time. This completion handler needs to be called, passing in either true or false, to indicate whether the closure was able to successfully carry out it’s intended action … and no, I’ve got no idea why or what this achieves, and the documentation is less than illuminating on the matter.
  • the order the actions in the array used to initialise the UISwipeActionsConfiguration object dictates the order they are displayed in: the last item in the array will be the leftmost button on the swipe action. It will also be the default action if the user swipes the cell all the way across.

One final thing worth noting: return an empty array to disable swipe actions for a row

return UISwipeActionsConfiguration(actions: [])

1 thought on “Updating UITableView Swipe Actions for iOS 11+”

Leave a Reply