Full Source code: https://github.com/boctor/idev-recipes/tree/master/SideSwipeTableView

Problem:

The Twitter iPhone app pioneered the ability to swipe on a tweet and have a menu appear, letting you do things like reply or favorite the tweet.

Tweets in the Twitter app are table view cells in a table view. How do we recreate this feature and add the ability to side swipe on table view cells?

Solution:

This feature has two distinct parts. The first is detecting that the user swiped on the table. The second is animating in and animating out the menu view.

Detecting swipes in iOS 4

iOS 4 introduced Gesture Recognizers which make gestures like swiping, tapping and pinching very east to detect. Specifically we can create a couple of UISwipeGestureRecognizer objects, one for the right direction and another for the left detection and attach them to the table view:

// Setup a right swipe gesture recognizer
UISwipeGestureRecognizer* rightSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeRight:)] autorelease];
rightSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
[tableView addGestureRecognizer:rightSwipeGestureRecognizer];

// Setup a left swipe gesture recognizer
UISwipeGestureRecognizer* leftSwipeGestureRecognizer = [[[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(swipeLeft:)] autorelease];
leftSwipeGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
[tableView addGestureRecognizer:leftSwipeGestureRecognizer];

Now when the user swipes left or right anywhere in the table, our swipeRight: or swipeLeft: methods will get called. The touch handling code that tracks the user’s finger and figures out their intent is blissfully handled for us.

Detecting swipes in iOS 3

When this feature was introduced in what was then the Tweetie app, it worked in iOS 3 well before iOS 4 was released. You might smartly argue that if iOS 3 currently makes up 1-2% of users out there it isn’t worth developing for and I admit this is a valid point. Still it’s an interesting technical mystery and that’s just the kind of thing we love solving!

You might have thought like I did, that the Twitter app must have implemented its own touch handling, guessing based on the location of your finger whether you were trying to do a swipe, but this is wrong.

In the Twitter app it doesn’t matter if you swipe left or swipe right, the animation of the menu always happens from left to right. This is the same behavior as the editing of table view cells and it turns out this is how the Twitter app does it: It hijacks the built in swipe to delete feature of table view cells. There are 3 parts to making this work:

1. Enabling swipe to delete

Per Apple’s documentation:

To enable the swipe-to-delete feature of table views (wherein a user swipes horizontally across a row to display a Delete button), you must implement the tableView:commitEditingStyle:forRowAtIndexPath: method

So the first step is to implement the tableView:commitEditingStyle:forRowAtIndexPath: method.

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
}

The method doesn’t have to do anything. Once it is implemented, you’ll be able to side swipe on a cell and the Delete button will appear.

2. Disabling the Delete button

Apple’s Inserting and Deleting Rows and Sections documentation indicates that when you explicitly put a table in editing mode by calling setEditing:animated:, the same message is then sent to each of the visible cells.

The documentation for a table view cell’s setEditing:animated: indicates that when this method is called, insertion/deletion control are animated in.

So disabling the Delete button turns out to be relatively simple: Override the table view cell’s setEditing:animated: and don’t call the superclass’s implementation.

- (void)setEditing:(BOOL)editing animated:(BOOL)animated
{
  // We suppress the Delete button by explicitly not calling
  // super's implementation
  if (supressDeleteButton)
  {
    // Reset the editing state of the table back to NO
    UITableView* tableView = [self getTableView:self];
    tableView.editing = NO;
  }
  else
    [super setEditing:editing animated:animated];
}

3. Getting notified when a swipe occurred

Apple’s docs for tableView:willBeginEditingRowAtIndexPath: are crystal clear: This method is called when the user swipes horizontally across a row.

The implementation of this method in iOS 3 is a parallel to the swipeLeft: and swipeRight: methods we registered with UISwipeGestureRecognizers under iOS 4. When any of these methods are called, we know that a swipe happened and we are ready to animate in the menu.

Animating in the menu view

Before we animate in the menu, we first add it as a subview of the table view.

As you can see in the image at the top of this post, we are animating the existing cell content offscreen while simultaneously animating in the menu. Here is a rough illustration of how both the cell content and the menu have to animate in sync during a left to right animation:

So we first set the frame of the menu, placing it offscreen. Depending of the direction, we’d put it offscreen on the right or left side of the table. Next we’d start an animation block and set the frame of the menu to be at 0 x-offset. Inside the same animation block we also set the cell’s frame to be offscreen on the other side of the table.

- (void) addSwipeViewTo:(UITableViewCell*)cell direction:(UISwipeGestureRecognizerDirection)direction
{
  // Change the frame of the side swipe view to match the cell
  sideSwipeView.frame = cell.frame;

  // Add the side swipe view to the table
  [tableView addSubview:sideSwipeView];
  
  // Remember which cell the side swipe view is displayed on and the swipe direction
  self.sideSwipeCell = cell;
  sideSwipeDirection = direction;

  // Move the side swipe view offscreen either to the left or the right depending on the swipe direction
  CGRect cellFrame = cell.frame;
  sideSwipeView.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? -cellFrame.size.width : cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);

  // Animate in the side swipe view
  animatingSideSwipe = YES;
  [UIView beginAnimations:nil context:nil];
  [UIView setAnimationDuration:0.2];
  [UIView setAnimationDelegate:self];
  [UIView setAnimationDidStopSelector:@selector(animationDidStopAddingSwipeView:finished:context:)];
  // Move the side swipe view to offset 0
  sideSwipeView.frame = CGRectMake(0, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
  // While simultaneously moving the cell's frame offscreen
  // The net effect is that the side swipe view is pushing the cell offscreen
  cell.frame = CGRectMake(direction == UISwipeGestureRecognizerDirectionRight ? cellFrame.size.width : -cellFrame.size.width, cellFrame.origin.y, cellFrame.size.width, cellFrame.size.height);
  [UIView commitAnimations];
}

Animating out the menu view

When the menu is animated away, there is a little bounce of the cell content as it comes back into view.

Here is another rough illustration showing the three animations that make up a bounce, showing at each step where the cell content and menu are:

There are multiple ways we can achieve this animation. One is CAKeyframeAnimation where you specify a path and the animation follows that path.

Instead the code simply chains together the 3 separate animations. Since we might care about iOS 3, we don’t use animation blocks, but instead use begin/commit animation methods and register an animation stop selector where we start the next animation.

Just like we did when animating the menu in, at each step we animate both the menu as well as the cell to give the illusion that the cell content is pushing the menu out of view.

UPDATE: It was pointed out on Hacker News that the Twitter app actually puts the menu behind the cell and then only animates the cell content in and out. The menu isn’t animated at all. I’ve updated the code so that by default it now does this style of animation. If you really liked the pushing behavior where both the menu and cell content are animated, there is a PUSH_STYLE_ANIMATION #define that you can set to YES to get it back.

Full Source code: https://github.com/boctor/idev-recipes/tree/master/SideSwipeTableView

Tweet This!Hacker NewsShare on Facebook