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
Adding actions to the buttons
If you're happy with the log messages, feel free to skip to the next section. However, if you’d like something a little more tangible, you can add some handling to show the included DetailViewController when one of the delegate methods is called.
Add the following two methods to MasterViewController.m:
- (void)showDetailWithText:(NSString *)detailText
{
//1
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
DetailViewController *detail = [storyboard instantiateViewControllerWithIdentifier:@"DetailViewController"];
detail.title = @"In the delegate!";
detail.detailItem = detailText;
//2
UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:detail];
//3
UIBarButtonItem *done = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(closeModal)];
[detail.navigationItem setRightBarButtonItem:done];
[self presentViewController:navController animated:YES completion:nil];
}
//4
- (void)closeModal
{
[self dismissViewControllerAnimated:YES completion:nil];
}
You perform four actions in the code above:
- Grab the detail view controller out of the storyboard and set its title and detail item for display.
- Set up a
UINavigationController
to contain the detail view controller and to give you a place to add the close button. - Add the close button with a target within the
MasterViewController
. - Set up the actual target for the close button, which dismisses any modal view controller.
Next, replace the methods you added earlier with the following implementations:
- (void)buttonOneActionForItemText:(NSString *)itemText
{
[self showDetailWithText:[NSString stringWithFormat:@"Clicked button one for %@", itemText]];
}
- (void)buttonTwoActionForItemText:(NSString *)itemText
{
[self showDetailWithText:[NSString stringWithFormat:@"Clicked button two for %@", itemText]];
}
Finally, open Main.storyboard and click on the Detail View Controller. Select the Identity Inspector and set the Storyboard ID to DetailViewController to match the class name, like so:
If you forget this step, instantiateViewControllerWithIdentifier
will crash on an invalid argument exception stating that a view controller with that identifier doesn’t exist.
Build and run the application; click one of the buttons in a cell, and watch your modal view controller appear, as shown in the following screenshot:
Adding the Top Views And The Swipe Action
Now that you have the bottom part of the view working, it’s time to get the top portion up and running.
Open Main.storyboard and drag a UIView
into your SwipeableTableCell. The view should take up the entire height and width of the cell and cover your buttons so you won't able to see them until you get the swipe working.
If you want to be precise, you can open the Size Inspector and set the view's width and height to 320 and 43, respectively:
You'll also need a constraint to pin the view to the edges of the content view. Select the view and click the Pin button. Select all four spacing constraints and set their values to 0 as shown below:
Hook this new view up to its outlet by following the same steps as before: right-click the swipe able cell in the navigator on the left and drag from the myContentView outlet to the new view.
Next, drag a UILabel into the view; pin it 20 points from the left side of the view and center it vertically. Hook this label up to the myTextLabel outlet.
Build and run your application; your cells are looking somewhat normal again:
Adding the data
But why is the actual cell text data not showing up? That’s because you’re only assigning the itemText
to a property rather than doing anything that affects myTextLabel
.
Open SwipeableCell.m and add the following method:
- (void)setItemText:(NSString *)itemText {
//Update the instance variable
_itemText = itemText;
//Set the text to the custom label.
self.myTextLabel.text = _itemText;
}
This is an override of the default setter for the itemText
property.
Aside from updating the backing instance variable, the above method also updates the visible label.
Finally, to make the result of the next few steps a little easier to see, you’re going to make the title of the item a little longer so that some text will still be visible when the cell is swiped.
Head back to MasterViewController.m and update the following line in viewDidLoad
where the item titles are generated:
NSString *item = [NSString stringWithFormat:@"Longer Title Item #%d", i];
Build and run your application; you can now see the appropriate item titles as shown below:
Gesture recognisers - go!
Now here comes the “fun” part — building up the math, the constraints, and the gesture recognizers that facilitate the swiping action.
First, add the following properties to your SwipeableCell
class extension at the top of SwipeableCell.m:
@property (nonatomic, strong) UIPanGestureRecognizer *panRecognizer;
@property (nonatomic, assign) CGPoint panStartPoint;
@property (nonatomic, assign) CGFloat startingRightLayoutConstraintConstant;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewRightConstraint;
@property (nonatomic, weak) IBOutlet NSLayoutConstraint *contentViewLeftConstraint;
The short version of what you’re going to be doing is to track a pan gesture and then adjust the left and right constraints on your view based on a) how far the user has panned the cell and b) where the cell was when it started.
In order to do that, you’ll first need to hook up the IBOutlets for the left and right constraints of the myContentView view. These constraints pin that view to the cell’s contentView.
You can figure out which constraints these are by flipping open the list of constraints and examining which ones light up as you go through the list until you find the appropriate ones. In this case, it's the constraint between the right side of myContentView and the main contentView as shown below:
Once you’ve located the appropriate constraint, hook up the appropriate outlet — in this case, it's the contentViewRightConstraint, as such:
Follow the same steps to hook up the contentViewLeftConstraint to the constraint between the left side of myContentView and the main contentView.
Next, open SwipeableCell.m and modify the @interface
statement for the class extension category so that it conforms to the UIGestureRecognizerDelegate
protocol as follows:
@interface SwipeableCell() <UIGestureRecognizerDelegate>
Then, still in SwipeableCell.m, add the following method:
- (void)awakeFromNib {
[super awakeFromNib];
self.panRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panThisCell:)];
self.panRecognizer.delegate = self;
[self.myContentView addGestureRecognizer:self.panRecognizer];
}
This sets up the pan gesture recognizer and adds it to the cell.
Also add the following method:
- (void)panThisCell:(UIPanGestureRecognizer *)recognizer {
switch (recognizer.state) {
case UIGestureRecognizerStateBegan:
self.panStartPoint = [recognizer translationInView:self.myContentView];
NSLog(@"Pan Began at %@", NSStringFromCGPoint(self.panStartPoint));
break;
case UIGestureRecognizerStateChanged: {
CGPoint currentPoint = [recognizer translationInView:self.myContentView];
CGFloat deltaX = currentPoint.x - self.panStartPoint.x;
NSLog(@"Pan Moved %f", deltaX);
}
break;
case UIGestureRecognizerStateEnded:
NSLog(@"Pan Ended");
break;
case UIGestureRecognizerStateCancelled:
NSLog(@"Pan Cancelled");
break;
default:
break;
}
}
This is the method that's called when the pan gesture recogniser fires. For now, it simply logs the pan gesture details to the console.
Build and run your application; drag your finger across the cell and you’ll see all the logs firing with the movement, like so:
You’ll see positive numbers if you swipe to the right of your initial touch point, and negative numbers if you swipe to the left of your initial touch point. These numbers will be used to adjust the constraints of myContentView
.