Overview
One of the new but little discussed APIs in iOS 11 allows the addition of swipe actions on UITableView rows via the new UISwipeActionsConfiguration class and associated UITableViewDelegate
methods. Adding swipe left or swipe right actions is now pretty simple, so lets just dive right in. To try out these new APIs, a very basic list view was created with some hard coded data. This app will allow the user to swipe right to make a row “read” or “unread” and swipe left to either “flag” a row, or “delete” the row from the list. The code for this post can be found here.
The UITableView delegate
The keys to adding swipe actions start with new UITableView
delegate methods, defined as follows:
func tableView(UITableView, leadingSwipeActionsConfigurationForRowAt: IndexPath) -> UISwipeActionsConfiguration func tableView(UITableView, trailingSwipeActionsConfigurationForRowAt: IndexPath) -> UISwipeActionsConfiguration
As you can see in the method signatures, these both return a UISwipeActionsConfiguration
class. This class is initialized with an array of UIContextualAction
classes. So let’s start at the bottom and work our way up.
The Details
For our sample app, we’ll create a struct called an Email. It looks like this:
struct Email { let subject: String let body: String var isNew: Bool var isFlagged: Bool static func mockData(numberOfItems count: Int) -> [Email] { var emails = [Email]() for idx in 1...count { let email = Email(subject: "Email \(idx)", body: "This is my body for email \(idx)", isNew: true, isFlagged: false) emails.append(email) } return emails } mutating func toggleReadFlag() -> Bool { self.isNew = !self.isNew //normally make some call to toggle and return success/fail return true } mutating func toggleFlaggedFlag() -> Bool { self.isFlagged = !self.isFlagged //normally make some call to toggle and return success/fail return true } }
We will use this class to load up the table view with data. This is accomplished using the typical UITableViewDataSource
methods and can be seen within the ViewController
class. The real area of interest is within the UITableViewDelegate
. Here we introduce the 2 new methods mentioned above.
Setting up the UISwipeActionsConfiguration
Let’s take a deeper dive into the trailing actions method, since it will apply more than one possible swipe action.
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { let deleteAction = self.contextualDeleteAction(forRowAtIndexPath: indexPath) let flagAction = self.contextualToggleFlagAction(forRowAtIndexPath: indexPath) let swipeConfig = UISwipeActionsConfiguration(actions: [deleteAction, flagAction]) return swipeConfig }
Here, we are using a helper method to instantiate each of the UIContextualAction
classes we will need to build our swipe actions. Then we can instantiate a UISwipeActionsConfiguration
using the UIContextualAction
classes to populate the actions array. The UISwipeActionsConfiguration
is then returned from the delegate method.
The important thing to note here, is that the order of the items in the action array determines the order of the actions displayed when the table view row is swiped. The easiest way to remember is that the first item displays furtherest out, and subsequent items display moving towards the cell. So if we are dealing with trailing actions, the first item in the array corresponds to the rightmost action (“Delete”), the second one will be one inside of that (“Flag”), and so on.
Handling Contextual Actions
Next, lets take a look at the UIContextualAction
. This is where the work happens. The documentation found here. The initializer takes a UIContextualAction.Style
, title, and UIContextualActionHandler
block. The UIContextualActionHandler
gives you access to the UIContextualAction
, the view that displayed the action, and a completion handler that you pass a bool indicating whether or not the action was successful. An example can be found in the contextualToggleFlagAction
method shown below:
func contextualToggleFlagAction(forRowAtIndexPath indexPath: IndexPath) -> UIContextualAction { // 1 var email = data[indexPath.row] // 2 let action = UIContextualAction(style: .normal, title: "Flag") { (contextAction: UIContextualAction, sourceView: UIView, completionHandler: (Bool) -> Void) in // 3 if email.toggleFlaggedFlag() { // 4 self.data[indexPath.row] = email self.tableView.reloadRows(at: [indexPath], with: .none) // 5 completionHandler(true) } else { // 6 completionHandler(false) } } // 7 action.image = UIImage(named: "flag") action.backgroundColor = email.isFlagged ? UIColor.gray : UIColor.orange return action }
In this method:
- We are simply retrieving the data element from our data array
- Instantiating the UIContextualAction class, providing a style, title and handler
- Mutating the object. Of course our email struct could have been a class to ease the mutation handling.
- Saving the mutated object back to our data and reloading the appropriate rows.
- Call the provided completion handler, indicating success.
- Had the update to the object failed, we can indicate that to the completion handler by passing false.
- Setting the image and color attributes on action. By specifying an image, the title in the initializer is not displayed. This certainly makes localization a bit more work, since the images would need to contain any text we wanted to display for an action, assuming we wanted both text and images.
One more thing of note is that on the UISwipeActionsConfiguration
class, you can specify whether the first action in the collection should be performed with a full swipe. This defaults to true. Here is a video of the working app with both leading and trailing swipe actions.
https://youtu.be/XJ_Yd8ZC4d4