How To Make A Swipeable Table View Cell With Actions – Without Going Nuts With Scroll Views

So you want to make a swipeable table view cell like in Mail.app? This tutorial shows you how without getting bogged down in nested scroll views. By Ellen Shapiro.

Leave a rating/review
Save for later
Share
You are currently viewing page 6 of 6 of this article. Click here to view the first page.

Playing Nicer With The Table View

There's just a few more steps before you’re done!

First, your UIPanGestureRecognizer can sometimes interfere with the one which handles the scroll action on the UITableView. Since you’ve already set up the cell to be the pan gesture recognizer's UIGestureRecognizerDelegate, you only have to implement one (comically verbosely named) delegate method to make this work.

Add the following method to SwipeableCell.m:

#pragma mark - UIGestureRecognizerDelegate
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer
{
   return YES;
}

This method tells the gesture recognizers that they can both work at the same time.

Build and run your application; open the first cell and you can now scroll the tableview.

There's still an issue with cell reuse: rows don't remember their state, so as cells are reused their opened/closed state in the view won't reflect the actions of the user. To see this, open a cell, then scroll the table a bit. You'll notice that one cell always remains open, but it's a different one each time.

To fix the first half of this issue, add the following method to SwipeableCell.m:

- (void)prepareForReuse {
  [super prepareForReuse];
  [self resetConstraintContstantsToZero:NO notifyDelegateDidClose:NO];
}

This method ensures the cell re-closes before it's recycled.

To solve the second half of the issue, you’re going to add a public method to the cell to facilitate its opening. Then you’ll add some delegate methods to allow MasterViewController to manage which cells are open.

Open SwipeableCell.h. In the SwipeableCellDelegate protocol declaration, add the following two new methods below the existing methods:

- (void)cellDidOpen:(UITableViewCell *)cell;
- (void)cellDidClose:(UITableViewCell *)cell;

These methods will notify the delegate — in your case, the master view controller — that a cell has opened or closed.

Add the following public method declaration in the @interface declaration for SwipeableCell:

- (void)openCell;

Next, open SwipeableCell.m and add the following implementation for openCell:

- (void)openCell {
  [self setConstraintsToShowAllButtons:NO notifyDelegateDidOpen:NO];
}

This method allows the delegate to change the state of a cell.

Still working in the same file, find resetConstraintsToZero:notifyDelegateDidOpen: and replace the TODO at the top of the method with the following code:

if (notifyDelegate) {
  [self.delegate cellDidClose:self];
}

Next, find setConstraintsToShowAllButtons:notifyDelegateDidClose: and replace the TODO at the top of that method with the following code:

if (notifyDelegate) {
  [self.delegate cellDidOpen:self];
}

These two changes notify the delegate when a swipe gesture has completed and the cell has either opened or closed the menu.

Add the following property declaration to the top of MasterViewController.m, inside the class extension category:

@property (nonatomic, strong) NSMutableSet *cellsCurrentlyEditing;

This stores a list of cells that are currently open.

Add the following code to the end of viewDidLoad:

self.cellsCurrentlyEditing = [NSMutableSet new];

This initializes the set so you can add things to it later.

Now add the following methods to the same file:

- (void)cellDidOpen:(UITableViewCell *)cell {
  NSIndexPath *currentEditingIndexPath = [self.tableView indexPathForCell:cell];
  [self.cellsCurrentlyEditing addObject:currentEditingIndexPath];
}

- (void)cellDidClose:(UITableViewCell *)cell {
  [self.cellsCurrentlyEditing removeObject:[self.tableView indexPathForCell:cell]];
}

Note that you’re adding the index paths rather than the cells themselves to the list of cells currently editing. If you added the cell objects directly, then you’d see the same issue where the cells would appear open as they are recycled. With this method, you’ll be able to open the cells at the appropriate index paths instead.

Finally, add the following lines to tableView:cellForRowAtIndexPath: just before the final return statement:

if ([self.cellsCurrentlyEditing containsObject:indexPath]) {
  [cell openCell];
}

If the current cell's index path is in the set, it should be set to open.

Build & run the application. That’s it! You now have a table view that scrolls, maintains the open and closed state of cells, and uses delegate methods to launch arbitrary tasks from button taps in any cell.

Where To Go From Here

The final project is available here as a download. I’ll be working with what I’ve developed here to assemble an open source project to make things a bit more flexible - I’ll be posting a link in the forums when it’s ready to roll.

Any time you're you’re trying to replicate something Apple did without knowing exactly how they did it, you'll find that there are many, many ways to do it. This is just one solution to this problem; however, it’s one of the only solutions I’ve found that doesn’t involve lots of crazy mucking around with nested scroll views and the resulting gesture recognizer collisions that can get extremely hairy to untangle! :]

A couple of resources that were very helpful in writing this article, but which ultimately took very different approaches, were Ash Furrow’s article that got the entire ball rolling, and Massimiliano Bigatti’s BMXSwipeableCell project which showed just how deep the rabbit hole can go with the UIScrollView approach.

If you have any suggestions, questions, or related pieces of code, fire away in the comments!

Contributors

Over 300 content creators. Join our team.