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 4 of 6 of this article. Click here to view the first page.

Moving those constraints

Essentially, you need to push myContentView over to the left by adjusting the left and right constraints that pin it to the cell’s contentView. The right constraint will take a positive value, and the left constraint will take an equal but negative value.

For instance, if myContentView needs to be moved 5 points to the left, then the right constraint will take a value of 5 and the left constraint will take a value of -5. This slides the entire view over to the left by 5 points without changing its width.

Sounds easy — but there's a lot of moving parts to watch out for. You have to handle a whole lot of things very differently depending on whether the cell is already open or not, and what direction the user is panning.

You also need to know how far the cell is allowed to slide open. To do this, you’ll have to calculate the width of the area covered by the buttons. The easiest way is to subtract the minimum X position of the leftmost button from the full width of the view.

To clarify, here's a sneak peek ahead to more clearly illustrate the dimensions you'll need to be concerned with:

Minimum x of button 2

Luckily, thanks to the CGRect CGGeometry functions, this is super-easy to translate into code.

Add the following method to SwipeableCell.m:

- (CGFloat)buttonTotalWidth {
    return CGRectGetWidth(self.frame) - CGRectGetMinX(self.button2.frame);
}

Add the following two skeleton methods to SwipeableCell.m:

- (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)endEditing
{
	//TODO: Build.
}

- (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate
{
	//TODO: Build
}

These two skeleton methods — once you flesh them out — will snap the cell open and snap the cell closed. You’ll come back to these in a bit once you’ve added more handling in the pan gesture recognizer.

Replace the UIGestureRecognizerStateBegan case of panThisCell: with the following code:

case UIGestureRecognizerStateBegan:
  self.panStartPoint = [recognizer translationInView:self.myContentView];	           
  self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
  break;

You need to store the initial position of the cell (i.e. the constraint value), to determine whether the cell is opening or closing.

Next you need to start adding more handling for when the pan gesture recognizer has changed. Still in, panThisCell:, change the UIGestureRecognizerStateChanged case to look like this:

case UIGestureRecognizerStateChanged: { 
  CGPoint currentPoint = [recognizer translationInView:self.myContentView];
  CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
  BOOL panningLeft = NO; 
  if (currentPoint.x < self.panStartPoint.x) {  //1
    panningLeft = YES;
  }

  if (self.startingRightLayoutConstraintConstant == 0) { //2
    //The cell was closed and is now opening
    if (!panningLeft) {
      CGFloat constant = MAX(-deltaX, 0); //3
      if (constant == 0) { //4
        [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO];
      } else { //5
        self.contentViewRightConstraint.constant = constant;
      }
    } else {
      CGFloat constant = MIN(-deltaX, [self buttonTotalWidth]); //6
      if (constant == [self buttonTotalWidth]) { //7
        [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO];
      } else { //8
        self.contentViewRightConstraint.constant = constant;
      }
    }
  }

Most of the code above deals with pan gestures starting from cells in their default "closed" state. Here's what's going on in detail:

  1. Here you determine whether you’re presently panning to the left or the right of your original pan point.
  2. If the right layout constraint’s constant is equal to zero, that means myContentView is flush up against the contentView. Therefore the cell must be closed at this point and the user is attempting to open it.
  3. This is the case where the user swipes from left to right to close the cell. Rather than just saying “you can’t do that”, you have to handle the case where the user swipes the cell open a bit then wants to swipe it closed without having lifted their finger to end the gesture.
     
    Since a left-to-right swipe results in a positive value for deltaX and the right-to-left swipe will result in a negative value, you must calculate the constant to set on the right constraint based on the negative of deltaX. The maximum of this and zero is taken, so that the view can't go too far off to the right.
  4. If the constant is zero, the cell is being closed completely. Fire the method that handles closing — which, as you’ll recall, does nothing at the moment.
  5. If the constant is not zero, then you should set it to the right-hand side constraint.
  6. Otherwise, if you’re panning right to left, the user is attempting to open the cell. In this case, the constant will be the lesser of either the negative value of deltaX or the total width of both buttons.
  7. If the target constant is the total width of both buttons, the cell is being opened to the catch point and you should fire the method that handles opening.
  8. If the constant is not the total width of both buttons, then set the constant to the right constraint’s constant.

Phew! That’s a lot of handling…and that’s just for the case where the cell was already closed. You now need the code to handle the case when the cell is partially open when the gesture starts.

Add the following code directly below the code you just added:

  else {
    //The cell was at least partially open.
    CGFloat adjustment = self.startingRightLayoutConstraintConstant - deltaX; //1
    if (!panningLeft) {
      CGFloat constant = MAX(adjustment, 0); //2
      if (constant == 0) { //3
        [self resetConstraintContstantsToZero:YES notifyDelegateDidClose:NO];
      } else { //4
        self.contentViewRightConstraint.constant = constant;
      }
    } else {
      CGFloat constant = MIN(adjustment, [self buttonTotalWidth]); //5
      if (constant == [self buttonTotalWidth]) { //6
        [self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:NO];
      } else { //7
        self.contentViewRightConstraint.constant = constant;
      }
    }
  }

  self.contentViewLeftConstraint.constant = -self.contentViewRightConstraint.constant; //8
}
    break;

This is the other side of the outer if-statement. It is therefore the case where the cell is initially open.

Once again, here's an explanation of the various cases you're handling:

  1. In this case, you’re not just taking the deltaX - you’re subtracting deltaX from the original position of the rightLayoutConstraint to see how much of an adjustment has been made.
  2. If the user is panning left to right, you must take the greater of the adjustment or 0. If the adjustment has veered into negative numbers, that means the user has swiped beyond the edge of the cell, and the cell is closed, which leads you to the next case.
  3. If you’re seeing the constant equal to 0, the cell is closed and you must fire the method that handles closing the cell.
  4. Otherwise, you set the constant to the right constraint.
  5. In the case of panning right to left, you’ll want to take the lesser of the adjustment and the total button width. If the adjustment is higher, then the user has swiped too far past the catch point.
  6. If you’re seeing the constant equal to the total button width, the cell is open, and you must fire the method that handles opening the cell.
  7. Otherwise, set the constant to the right constraint.
  8. Now, you’re finally out of both the “cell was closed” and “cell was at least partially open” conditions, and you can do the same thing to the left constraint’s constant in any of these cases: set it to the negative value of the right constraint’s constant. This ensures the width of myContentView stays consistent no matter what you’ve had to do to the right constraint.

Build and run your application; you can now pan the cell back and forth! It’s not super-smooth, and it stops a little bit before you’d like it to. This is because you haven’t yet implemented the two methods that handle opening and closing the cell.

Note: You may also notice that the table view itself doesn’t scroll at the moment. Don’t worry. Once you’ve got the cells sliding open properly, you’ll fix that.

Contributors

Over 300 content creators. Join our team.