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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make A Swipeable Table View Cell With Actions – Without Going Nuts With Scroll Views
60 mins
- Getting Started
- Digging into the View Hierarchy
- A List Of Ingredients for a Swipeable Table View Cell
- Creating the Custom Cell
- Adding a delegate
- Adding actions to the buttons
- Adding the Top Views And The Swipe Action
- Adding the data
- Gesture recognisers - go!
- Moving those constraints
- Snap!
- Playing Nicer With The Table View
- Where To Go From Here
Snap!
Next up, you need to make the cell snap into place as appropriate. You'll notice at the moment that the cell just stops if you let go.
Before you get into the methods that handle this, you'll need a single method to create an animation.
Open SwipeableCell.m and add the following method:
- (void)updateConstraintsIfNeeded:(BOOL)animated completion:(void (^)(BOOL finished))completion {
float duration = 0;
if (animated) {
duration = 0.1;
}
[UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
[self layoutIfNeeded];
} completion:completion];
}
Next, you’ll need to flesh out the two skeleton methods that open and close the cell. Remember that in the original implementation, there’s a bit of a bounce since it uses a UIScrollView
subclass as one of the lowest z-index superviews.
To make things look right, you'll need to give your cell a bit of a bounce when it hits either edge. You’ll also have to ensure your contentView
and myContentView
have the same backgroundColor
for the optical illusion of the bounce to look as seamless as possible.
Add the following constant to the top of SwipeableCell.m, just underneath the import statement:
static CGFloat const kBounceValue = 20.0f;
This constant stores the bounce value to be used in all your bounce animations.
Update setConstraintsToShowAllButtons:notifyDelegateDidOpen:
as follows:
- (void)setConstraintsToShowAllButtons:(BOOL)animated notifyDelegateDidOpen:(BOOL)notifyDelegate {
//TODO: Notify delegate.
//1
if (self.startingRightLayoutConstraintConstant == [self buttonTotalWidth] &&
self.contentViewRightConstraint.constant == [self buttonTotalWidth]) {
return;
}
//2
self.contentViewLeftConstraint.constant = -[self buttonTotalWidth] - kBounceValue;
self.contentViewRightConstraint.constant = [self buttonTotalWidth] + kBounceValue;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) {
//3
self.contentViewLeftConstraint.constant = -[self buttonTotalWidth];
self.contentViewRightConstraint.constant = [self buttonTotalWidth];
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) {
//4
self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
This method executes when the cell should open up all the way. Here's what's going on:
- If the cell started open and the constraint is already at the full open value, just bail — otherwise the bouncing action will happen over and over and over again as you continue to swipe past the total button width.
- You initially set the constraints to be the combined value of the total button width and the bounce value, which pulls the cell a bit further to the left than it should go so that it can snap back. Then you fire off the animation for this setting.
- When the first animation completes, fire off a second animation which brings the cell to rest in an open position at exactly the button width.
- When the second animation completes, reset the starting constraint or you’ll see multiple bounces.
Update resetConstraintContstantsToZero:notifyDelegateDidClose:
as follows:
- (void)resetConstraintContstantsToZero:(BOOL)animated notifyDelegateDidClose:(BOOL)notifyDelegate {
//TODO: Notify delegate.
if (self.startingRightLayoutConstraintConstant == 0 &&
self.contentViewRightConstraint.constant == 0) {
//Already all the way closed, no bounce necessary
return;
}
self.contentViewRightConstraint.constant = -kBounceValue;
self.contentViewLeftConstraint.constant = kBounceValue;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) {
self.contentViewRightConstraint.constant = 0;
self.contentViewLeftConstraint.constant = 0;
[self updateConstraintsIfNeeded:animated completion:^(BOOL finished) {
self.startingRightLayoutConstraintConstant = self.contentViewRightConstraint.constant;
}];
}];
}
As you can see, this is similar to setConstraintsToShowAllButtons:notifyDelegateDidOpen:
, but the logic closes the cell instead of opening it.
Build and run your application; drag the cell all the way to its catch points. You'll see the bouncing action when you release the cell.
However, if you release the cell before either it’s fully open or fully closed, it’ll remain stuck in the middle. Whoops! You’re not handling the two cases of touches ending or being cancelled.
Find panThisCell:
and replace the handling for the UIGestureRecognizerStateEnded
case with the following:
case UIGestureRecognizerStateEnded:
if (self.startingRightLayoutConstraintConstant == 0) { //1
//Cell was opening
CGFloat halfOfButtonOne = CGRectGetWidth(self.button1.frame) / 2; //2
if (self.contentViewRightConstraint.constant >= halfOfButtonOne) { //3
//Open all the way
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
} else {
//Re-close
[self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
}
} else {
//Cell was closing
CGFloat buttonOnePlusHalfOfButton2 = CGRectGetWidth(self.button1.frame) + (CGRectGetWidth(self.button2.frame) / 2); //4
if (self.contentViewRightConstraint.constant >= buttonOnePlusHalfOfButton2) { //5
//Re-open all the way
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
} else {
//Close
[self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
}
}
break;
Here, you’re performing handling based on whether the cell was already open or closed as well as where the cell was when the pan gesture ended. In detail:
- Check whether the cell was already open or closed when the pan started by checking the starting right layout constraint.
- If the cell was closed and you are opening it, you want the point at which the cell automatically slides all the way open to be half of the width of the rightmost button — self.button1. Since you’re measuring against the constraint’s constant, you only need to calculate the actual width of the button itself, not its X position in the view.
- Next, test if the constraint has been opened past the point where you’d like the cell to open automatically. If it’s past that point, automatically open the cell. If it’s not, automatically close the cell.
- In the case where the cell starts as open, you want the point at which the cell will automatically snap closed to be a point more than halfway past the leftmost button. Add together the widths of any buttons which are not the leftmost button — in this case, just self.button1 — and half the width of the leftmost button — self.button2 — to find the point to check.
- Test if the constraint has moved past the point where you’d like the cell to close automatically. If it has, close the cell. If it hasn’t, re-open the cell.
Finally, you’ll need a bit of handling in case the touch event is cancelled. Replace the UIGestureRecognizerStateCancelled
case with the following:
case UIGestureRecognizerStateCancelled:
if (self.startingRightLayoutConstraintConstant == 0) {
//Cell was closed - reset everything to 0
[self resetConstraintContstantsToZero:YES notifyDelegateDidClose:YES];
} else {
//Cell was open - reset to the open state
[self setConstraintsToShowAllButtons:YES notifyDelegateDidOpen:YES];
}
break;
This handling is a bit more straightforward; since the user has cancelled the touch, they don’t want to change the existing state of the cell, so you just need to set everything back the way it was.
Build and run your application; swipe the cell and you’ll see that the cell snaps open and closed no matter where you lift your finger, as shown below: