How to Make a Gesture-Driven To-Do List App Like Clear: Part 3/3

Colin Eberhardt
Learn how to make a stylish gesture driven to-do app like Clear!

Learn how to make a stylish gesture driven to-do app like Clear!

This is a post by Tutorial Team Member Colin Eberhardt, CTO of ShinobiControls, creators of playful and powerful iOS controls. Check out their app, ShinobiPlay. You can find Colin on and Twitter

This is the last in a three-part tutorial series that walks you through creating a simple to-do list app free of buttons, toggle switches and other common user interface (UI) controls, opting for a purely gesture-driven approach that vastly simplifies your UI.

If you followed the first and second parts of this series, you should now have a to-do list application that supports swipe gestures for marking items as complete and for deleting them (accompanied by a funky “shuffle” animation). Your app also allows the user to edit items.

But there is one glaring omission in the app’s functionality – the user cannot add new items to the list! Of course, I’m not sure that’s such a bad thing – I hate adding new to-dos to my never-ending list. :]

A conventional approach to this problem would most likely be to add a button with the text “Add new” on a title bar. But remember to ask yourself every time you want to add a new UI control: can I perform the same function via a gesture?

I’m guessing that you know the answer in this case, as in most cases, is YES. Read on to learn how!

The Pull-to-Add Gesture

The gestures that feel the most natural tend to play on the illusion that the phone UI is a physical object that obeys the same laws of physics as the natural world. Deleting an item from the to-do list by “pulling” it off the side of the screen feels quite natural, in the same way that you might swiftly pull a straw out in a game of KerPlunk.

The pull-down gesture has become ubiquitous in mobile apps as a means to refresh a list. The pull-down gesture feels very much like you are pulling against the natural resistance of the list, as if it were a hanging rope, in order to physically pull more items in from the top. Again, it is a natural gesture that in some way reflects how things work in the “real” world.

There has been some concern about the legality of using the pull-to-refresh gesture, due to a user interface patent. However, the recent introduction of this feature in the iOS email application (with a gorgeous tear-drop effect), the iOS 6 SDK itself, and its popularity in the App Store means that developers are less concerned about this (ridiculous) patent.

Note: To learn more about iOS 6’s built-in pull-to-refresh control, check out Chapter 20 in iOS 6 by Tutorials, “What’s New with Cocoa Touch.”

Pulling down on the list to add a new item at the top is a great gesture to add to your to-do list application, so in this part of the tutorial, you’ll start with that!

You could add the new logic to your custom table implementation, SHCTableView. However, as more functionality is added, that class runs the risk of getting crowded, and the code hard to follow.

A better approach is to add the code that implements the pull-to-add gesture into a separate class. This code requires access to the scroll view delegate methods, so a suitable approach is to subclass SHCTableView.

Start by adding a new class to the project. Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class SHCTableViewDragAddNew, and make it a subclass of SHCTableView.

The UIScrollViewDelegate, which SHCTableView already adopts (and which your newly-added SHCTableViewDragAddNew also supports, by virtue of the fact that it is a subclass), has methods that allow you to perform actions as the user scrolls the list.

In order to implement a pull-to-add gesture, you first have to detect when the user has started to scroll while at the top of the list. Then, as the user pulls further down, position a placeholder element that indicates where the new item will be added.

SHCTableViewCell, which renders each item in the list, can be used as the placeholder. Do that by replacing the contents of SHCTableViewDragAddNew.m with the following:

#import "SHCTableViewDragAddNew.h"
#import "SHCTableViewCell.h"
 
@implementation SHCTableViewDragAddNew {    
    // a cell that is rendered as a placeholder to indicate where a new item is added
    SHCTableViewCell* _placeholderCell;
}
 
-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _placeholderCell = [[SHCTableViewCell alloc] init];
        _placeholderCell.backgroundColor = [UIColor redColor];
    }
    return self;
}
 
@end

The above code simply sets up the instance variable for the placeholder and initializes it when an instance of SHCTableViewDragAddNew is instantiated.

In order to use SHCTableViewCell as a placeholder, you need access to the label that renders the text for the to-do item. This means exposing the label on the cell as a property.

So open SHCTableViewCell.h and add an import for SHCStrikethroughLabel at the top of the same file:

#import "SHCStrikethroughLabel.h"

Then, add the following property below the ones you added previously:

// the label used to render the to-do text
@property (nonatomic, strong, readonly) SHCStrikethroughLabel* label;

Since you’re declaring the label as a property, you probably also want to remove the instance variable named _label that you have in SHCTableViewCell.m (it’s at the very beginning, along with the other instance variables).

Adding the placeholder when the pull gesture starts and maintaining its position is really quite straightforward. When dragging starts, check whether the user is currently at the start of the list, and if so, use a _pullDownInProgress instance variable to record this state.

Of course, you first have to add this new instance variable to SHCTableViewDragAddNew.m (it goes right below _placeholderCell):

    // indicates the state of this behavior
    BOOL _pullDownInProgress;

Now add the UIScrollViewDelegate method necessary to detect the beginning of a pull:

#pragma mark - UIScrollViewDelegate
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // this behaviour starts when a user pulls down while at the top of the table
    _pullDownInProgress = scrollView.contentOffset.y <= 0.0f;
    if (_pullDownInProgress) {
        // add your placeholder
        [self insertSubview:_placeholderCell atIndex:0];
    }
}

While a scroll is in progress, the placeholder is repositioned by setting its frame using another UIScrollViewDelegate method.

The cell height for each row in the table view is currently hard-coded within SHCTableView.m. But you need access to this value within the SHCTableViewDragAddNew subclass. So switch to SHCTableView.m and locate the following line:

const float SHC_ROW_HEIGHT = 50.0f;

Remove that line from SHCTableView.h. Then, open SHCTableView.h and add the following line just above the @interface declaration:

#define SHC_ROW_HEIGHT 50.0f

Why did you change the value from a constant to a #define? Because SHCTableView is included from several other files (or will be soon), the compiler will start complaining about duplicate symbols if you have the constant value. You don’t run into the same issue using the #define.

Actually, a better implementation might be to provide an optional delegate method that requests the row height, but this simple hard-coded approach will work for now.

Back in SHCTableViewDragAddNew.m, add an implementation for the scrollViewDidScroll: UIScrollViewDelegate method as follows:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [super scrollViewDidScroll:scrollView];
 
    if (_pullDownInProgress && self.scrollView.contentOffset.y <= 0.0f) {
        // maintain the location of the placeholder
        _placeholderCell.frame = CGRectMake(0, - self.scrollView.contentOffset.y - SHC_ROW_HEIGHT,
                                            self.frame.size.width, SHC_ROW_HEIGHT);
        _placeholderCell.label.text = -self.scrollView.contentOffset.y > SHC_ROW_HEIGHT ?
                @"Release to Add Item" : @"Pull to Add Item";
        _placeholderCell.alpha = MIN(1.0f, - self.scrollView.contentOffset.y / SHC_ROW_HEIGHT);
    } else {
        _pullDownInProgress = false;
    }
}

Note that since the superclass handles scrolling in order to recycle cells, the superclass implementation of scrollViewDidScroll: needs to be invoked. The rest of the code simply maintains the placeholder as the user scrolls.

When the user stops dragging, you need to check whether they pulled down far enough (i.e., by at least the height of a cell), and remove the placeholder. This is achieved by adding the implementation of scrollViewDidEndDragging::

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    // check whether the user pulled down far enough
    if (_pullDownInProgress && - self.scrollView.contentOffset.y > SHC_ROW_HEIGHT) {
        // TODO – add a new item
    }
    _pullDownInProgress = false;
    [_placeholderCell removeFromSuperview];
}

As you’ll notice, the code doesn’t actually insert a new item yet. Later on, you’ll take a look at the logic required to update your array of model objects.

As you’ve seen, implementing a pull-down gesture is really quite easy! Did you notice the way that the above code adjusts the placeholder alpha and flips its text from “Pull to Add Item” to “Release to Add Item”? These are contextual cues, as mentioned in Part 1 of this series (you do remember, don’t you?).

Before you can test this code, you need to update the view controller to make use of SHCTableViewDragAddNew rather than SHCTableView. Open SHCViewController.xib, select the table view and, using the Identity Inspector, change the custom class to SHCTableViewDragAddNew.

You also need to change the type of the outlet in SHCViewController.h. First add a new import statement:

#import "SHCTableViewDragAddNew.h"

The header defines a single property, so next, change the type:

@property (weak, nonatomic) IBOutlet SHCTableViewDragAddNew *tableView;

Once this is done, build and run to see your new gesture in action:

When the drag gesture is completed, you need to add a new SHCToDoItem to the array holding the to-do items. The table currently exposes a datasource, and considering that this is a “data” operation rather than a cosmetic feature, the data source is the best place to add the required methods.

Open SHCTableViewDataSource.h and add the following method prototype (before the @end):

// Informs the datasource that a new item has been added at the top of the table
-(void)itemAdded;

Then open SHCTableViewDragAddNew.m and replace the “TODO” in scrollViewDidEndDragging: (which you added previously) with the following:

[self.dataSource itemAdded];

The SHCViewController already adopts the datasource protocol, so go right ahead and add an implementation for the new method. Open SHCViewController.m and add the following method:

-(void)itemAdded {
    // create the new item
    SHCToDoItem* toDoItem = [[SHCToDoItem alloc] init];
    [_toDoItems insertObject:toDoItem atIndex:0];
    // refresh the table
    [_tableView reloadData];
    // enter edit mode
    SHCTableViewCell* editCell;
    for (SHCTableViewCell* cell in _tableView.visibleCells) {
        if (cell.todoItem == toDoItem) {
            editCell = cell;
            break;
        }
    }
    [editCell.label becomeFirstResponder];
}

This code is pretty simple – it adds a new to-do item to the start of the array, and then forces an update of the table. Then it locates the cell that renders this newly added to-do item and sends a becomeFirstResponder: message to the text label in order to go straight into edit mode.

The end result is that as soon as a new item is added, the user can start entering the description for their to-do item:

That’s pretty slick!

Time To Refactor

Part of me wants to steamroll ahead to the next gesture, pinch-to-add, which is going to be a lot of fun. But there is something about the current code that is bugging me.

In order to separate the pull-down code from the “core” table logic, which includes cell recycling, you have created a subclass and added the logic there. You could follow the same steps when the next gesture is added, subclassing SHCTableViewDragAddNew with a new class SHCTableViewDragAddNewAndPinchToAddNew, which is a bit verbose.

But it’s not just the naming that is a problem. What if you wanted to remove the pull-to-add gesture? The code is now sandwiched within a class hierarchy, and as a result is hard to extract.

Image courtesy of the stock.xchng user dreamtwist

Another design principle that is often cited is Composition over Inheritance. Applying this principle here, the pull-to-add gesture should not be implemented as a subclass; rather, it should be an entirely separate class that collaborates with (i.e., is composed with) the table view in order to add the required behavior.

Let’s start making these changes and see where it leads you.

Open SHCTableViewDragAddNew.h and change the superclass to NSObject as follows:

@interface SHCTableViewDragAddNew : NSObject

The class still needs to be associated with the table view in some way, so switch to SHCTableViewDragAddNew.m and add an SHCTableView instance variable to the class:

    // the table that this gesture is associated with
    SHCTableView* _tableView;

You need to supply an instance of SHCTableView to this class. Open SHCTableViewDragAddNew.h and add the following initializer prototype:

-(id)initWithTableView:(SHCTableView *)tableView;

You will no longer be creating this class via Interface Builder, so you can safely remove initWithCoder: in SHCTableViewDragAddNew.m and replace it with your new initializer:

-(id)initWithTableView:(SHCTableView *)tableView {
    self = [super init];
    if (self) {
        _placeholderCell = [[SHCTableViewCell alloc] init];
        _placeholderCell.backgroundColor = [UIColor redColor];
        _tableView = tableView;
    }
    return self;
}

At this point, you’ll probably notice that much of SHCTableViewDragAddNew does not compile. The previous code you added for the UIScrollViewDelegate methods makes references to self, assuming that this class is a SHCTableView subclass. You will need to change all of these self references to the newly added instance variable.

Here is the complete implementation of these delegate methods, just in case you get a bit lost:

-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // this behaviour starts when a user pulls down while at the top of the table
    _pullDownInProgress = scrollView.contentOffset.y <= 0.0f;
    if (_pullDownInProgress) {
        // add your placeholder
        [_tableView insertSubview:_placeholderCell atIndex:0];
    }
}
 
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (_pullDownInProgress && _tableView.scrollView.contentOffset.y <= 0.0f) {
        // maintain the location of the placeholder
        _placeholderCell.frame = CGRectMake(0, - _tableView.scrollView.contentOffset.y - SHC_ROW_HEIGHT,
                                            _tableView.frame.size.width, SHC_ROW_HEIGHT);
        _placeholderCell.label.text = -_tableView.scrollView.contentOffset.y > SHC_ROW_HEIGHT ?
        @"Release to Add Item" : @"Pull to Add Item";
        _placeholderCell.alpha = MIN(1.0f, - _tableView.scrollView.contentOffset.y / SHC_ROW_HEIGHT);
    } else {
        _pullDownInProgress = false;
    }
}
 
-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    // check whether the user pulled down far enough
    if (_pullDownInProgress && - _tableView.scrollView.contentOffset.y > SHC_ROW_HEIGHT) {
        [_tableView.dataSource itemAdded];
    }
    _pullDownInProgress = false;
    [_placeholderCell removeFromSuperview];
}

Next, you must change the view controller so that your UI has an instance of SHCTableView once again, and yes, this means undoing the previous steps. (Sorry!)

Select SHCViewController.xib, select the table and, using the Identity Inspector, change the custom class back to SHCTableView. Then switch to SHCViewController.h and change the type of the outlet back again as well:

@property (weak, nonatomic) IBOutlet SHCTableView *tableView;

Now you need some way to inform the SHCTableViewDragAddNew class of the scrolling behavior of the table.

The SHCTableView currently exposes its scroll view as a property, so you could assign SHCTableViewDragAddNew as the delegate. However, SHCTableView is already assigned to the scroll view delegate! It looks like you’re stuck, doesn’t it? Fortunately not!

If you look at UITableViewDelegate, you’ll see that it also conforms to (i.e., extends) UIScrollViewDelegate. So you could employ a similar approach here. Your custom table currently does not have a delegate, so let’s add one!

Open SHCTableView.h and add the following property:

@property (nonatomic, assign) id<UIScrollViewDelegate> delegate;

To make SHCTableViewDragAddNew use this delegate, open SHCTableViewDragAddNew.h and adopt the delegate protocol:

@interface SHCTableViewDragAddNew : NSObject <UIScrollViewDelegate>

Then set the delegate by opening SHCTableViewDragAddNew.m and adding the following to the end of initWithTableView: (within the if condition):

_tableView.delegate = self;

You need to redirect any messages sent to the scroll view delegate (i.e., the SHCTableView) to this delegate. The table already has an implementation of scrollViewDidScroll:, so you can easily forward that one to the new delegate.

Open SHCTableView.m and replace scrollViewDidScroll: with the following:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self refreshView];
    // forward the delegate method
    if ([self.delegate respondsToSelector:@selector(scrollViewDidScroll:)]){
        [self.delegate scrollViewDidScroll:scrollView];
    }    
}

Note: scrollViewDidScroll: is an optional protocol method, so the code first checks that the delegate has implemented that method before calling it.

You could go ahead and add a similar forwarding implementation for every single method in UIScrollViewDelegate, but that would be a repetitive and un-rewarding task (although if you want to practice your typing skills, be my guest!). A much more cunning approach is to make use of Objective-C message forwarding.

If the runtime fails to find a method that matches a message’s selector, there are various other ways you can handle the message. One possibility is to forward the message to another object. Using this technique, you can very easily give the table view delegate the opportunity to receive a message, before following the standard message routing.

Add the following methods to SHCTableView.m:

#pragma mark - UIScrollViewDelegate forwarding
-(BOOL)respondsToSelector:(SEL)aSelector {
    if ([self.delegate respondsToSelector:aSelector]) {
        return YES;
    }
    return [super respondsToSelector:aSelector];
}
 
 
-(id)forwardingTargetForSelector:(SEL)aSelector {
    if ([self.delegate respondsToSelector:aSelector]) {
        return self.delegate;
    }
    return [super forwardingTargetForSelector:aSelector];
}

forwardingTargetForSelector: is the method that performs the rerouting. When any UIScrollViewDelegate selectors are sent to the table view, it first checks whether the delegate responds to that selector, and if so, forwards the message. So, as an example, the scrollViewWillBeginDragging: selector will be rerouted to the delegate, which will be your SHCTableViewDragAddNew class.

This code is slightly confusing at first, but it is super powerful!

Image courtesy of the stock.xchng user storm110

Finally, you can reinstate the pull-to-add gesture by simply adding it as an instance variable within the app’s view controller. Open SHCViewController.m and add the instance variable:

SHCTableViewDragAddNew* _dragAddNew;

Create an instance of this class at the end of viewDidLoad::

_dragAddNew = [[SHCTableViewDragAddNew alloc] initWithTableView:self.tableView];

I would say that this is a much more elegant solution. :] The support for this gesture can now be added or removed with a single line of code, with the table view inheritance structure totally unaffected. Furthermore, the interface for SHCTableViewDragAddNew is simplicity itself, with a single init method and nothing more.

What are you waiting for? Build and run your app to make sure that everything works just as before!

The Pinch-To-Add Gesture

The final feature you’ll add to the app will allow the user to insert a new to-do item in the middle of the list. Designing an interface to achieve this sort of functionality without the use of gestures would probably result in something quite cluttered and clunky. In fact, for this very reason, there are not many apps that support a mid-list insert.

The pinch is a natural gesture for adding a new to-do item between two existing ones. It allows the user to quite literally part the list exactly where they want the new item to appear. To implement this feature, you’ll make use of UIPinchGestureRecognizer.

Add a new file to the project using the iOS\Cocoa Touch\Objective-C class template. Name it SHCTableViewPinchToAdd and make it a subclass of NSObject.

You’ll follow exactly the same pattern as with the refactored pull-to-add. Open SHCTableViewPinchToAdd.h and replace its contents with the following:

#import "SHCTableView.h"
 
// A behavior that adds the facility to pinch the list in order to insert a new
// item at any location.
@interface SHCTableViewPinchToAdd : NSObject
 
// associates this behavior with the given table
-(id)initWithTableView:(SHCTableView*)tableView;
 
@end

You’ll use an SHCTableViewCell as a placeholder once again, and create the gesture recognizer within the init method. Open SHCTableViewPinchToAdd.m and replace its contents with the following:

#import "SHCTableViewPinchToAdd.h"
#import "SHCTableViewCell.h"
 
@implementation SHCTableViewPinchToAdd {
    // the table that this class extends and adds behavior to
    SHCTableView* _tableView;
    // a cell which is rendered as a placeholder to indicate where a new item is added
    SHCTableViewCell* _placeholderCell;
}
 
-(id)initWithTableView:(SHCTableView*)tableView {
    self = [super init];
    if (self) {
        _placeholderCell = [[SHCTableViewCell alloc] init];
        _placeholderCell.backgroundColor = [UIColor redColor];
        _tableView = tableView;
        // add a pinch recognizer
        UIGestureRecognizer* recognizer = [[UIPinchGestureRecognizer alloc] initWithTarget:self action:@selector(handlePinch:)];
        [_tableView addGestureRecognizer:recognizer];
    }
    return self;
}
 
-(void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
    //TODO: Clever code goes here!
}
 
@end

The handlePinch method is called when a pinch gesture starts, changes (i.e., the user moves their finger), and ends. In order to allow the user to “part” the list, you need to detect when their fingers touch two neighboring to-do items before executing a pinch gesture.

Your first task is to detect the start of the pinch. Replace the empty handlePinch: implementation with the following code:

-(void)handlePinch:(UIPinchGestureRecognizer *)recognizer {
    if (recognizer.state == UIGestureRecognizerStateBegan) {
        [self pinchStarted:recognizer];
    }    
}
 
-(void) pinchStarted:(UIPinchGestureRecognizer *)recognizer {
    // ...
}

Before tackling the problem of detecting which to-do items were touched and whether they are neighbors, you’ll add a few utility methods. This class needs to pass pairs of touch points between methods, so it makes sense to create a structure in order to pass this information around more easily.

Add the following to SHCTableViewPinchToAdd.m before the @implementation line:

// represents the upper and lower points of a pinch
struct SHCTouchPoints {
    CGPoint upper;
    CGPoint lower;
};
typedef struct SHCTouchPoints SHCTouchPoints;

When the user executes a pinch, it doesn’t really matter which of the touch points is the upper or lower point. The points are simply named upper and lower for ease of reference.

You also need to consider the scroll offset when handling touch events. Add the following helper method to tackle this:

// returns the two touch points, ordering them to ensure that upper and lower
// are correctly identified.
-(SHCTouchPoints) getNormalisedTouchPoints: (UIGestureRecognizer*) recognizer {
    CGPoint pointOne = [recognizer locationOfTouch:0 inView:_tableView];
    CGPoint pointTwo = [recognizer locationOfTouch:1 inView:_tableView];
    // offset based on scroll
    pointOne.y += _tableView.scrollView.contentOffset.y;
    pointTwo.y += _tableView.scrollView.contentOffset.y;
    // ensure pointOne is the top-most
    if (pointOne.y > pointTwo.y) {
        CGPoint temp = pointOne;
        pointOne = pointTwo;
        pointTwo = temp;
    }
    SHCTouchPoints points = {pointOne, pointTwo};
    return points;
}

The final utility method you need is one that hit-tests a view to see whether it contains a point. This is as simple as checking whether the point “lands” within the frame. The cells are full-width, so this method only needs to check the y-direction.

Add the following method:

-(BOOL) viewContainsPoint:(UIView*)view withPoint:(CGPoint)point {
    CGRect frame = view.frame;
    return (frame.origin.y < point.y) && (frame.origin.y + frame.size.height) > point.y;
}

These utility methods help you locate the cells that are touched by the user and determine whether they are neighbors. You can now fill in the details for pinchStarted:. But before doing that, you need a few new instance variables. Add them at the top of the file:

    // the indices of the upper and lower cells that are being pinched
    int _pointOneCellindex;
    int _pointTwoCellindex;
 
    // the location of the touch points when the pinch began
    SHCTouchPoints _initialTouchPoints;
 
    // indicates that the pinch is in progress
    BOOL _pinchInProgress;
 
    // indicates that the pinch was big enough to cause a new item to be added
    BOOL _pinchExceededRequiredDistance;

Now replace the empty implementation for pinchStarted: with this one:

-(void) pinchStarted:(UIPinchGestureRecognizer *)recognizer {
    // find the touch-points
    _initialTouchPoints = [self getNormalisedTouchPoints:recognizer];
 
    // locate the cells that these points touch
    _pointOneCellindex = -100;
    _pointTwoCellindex = -100;
    NSArray* visibleCells = _tableView.visibleCells;
    for (int i=0; i < visibleCells.count; i++) {
        UIView* cell = (UIView*)visibleCells[i];
        if ([self viewContainsPoint:cell withPoint:_initialTouchPoints.upper]) {
            _pointOneCellindex = i;
            // highlight the cell – just for debugging!
            cell.backgroundColor = [UIColor purpleColor];
        }
        if ([self viewContainsPoint:cell withPoint:_initialTouchPoints.lower]) {
            _pointTwoCellindex = i;
            // highlight the cell – just for debugging!
            cell.backgroundColor = [UIColor purpleColor];
        }
    }
 
    // check whether they are neighbors
    if (abs(_pointOneCellindex - _pointTwoCellindex) == 1) {
        // if so - initiate the pinch
        _pinchInProgress = YES;
        _pinchExceededRequiredDistance = NO;
 
        // show your place-holder cell
        SHCTableViewCell* precedingCell = (SHCTableViewCell*)(_tableView.visibleCells)[_pointOneCellindex];
        _placeholderCell.frame = CGRectOffset(precedingCell.frame, 0.0f, SHC_ROW_HEIGHT / 2.0f);
        [_tableView.scrollView insertSubview:_placeholderCell atIndex:0];       
 
    }
}

As the inline comments indicate, the above code finds the initial touch points, locates the cells that were touched, and then checks if they are neighbors. This is simply a matter of comparing their indices. If they are neighbors, then the app displays the cell placeholder that shows it will insert the new cell.

This new interaction can be added with just a couple of lines of code. Open SHCViewController.m and add the new class as an instance variable. Also remember to import the header:

// Import header
#import "SHCTableViewPinchToAdd.h"
 
// Add instance variable under the @implementation section
SHCTableViewPinchToAdd* _pinchAddNew;

Then create an instance of this new interaction at the end of viewDidLoad::

    _pinchAddNew = [[SHCTableViewPinchToAdd alloc] initWithTableView:self.tableView];

Now build and run.

When developing multi-touch interactions, it really helps to add visual feedback for debugging purposes. In this case, it helps to ensure that the scroll offset is being correctly applied! If you place two fingers on the list, you will see the to-do items are highlighted purple:

Note: While it is possible to test the app on the Simulator, you might find it easier to test this part on device. If you do decide to use the Simulator, you can hold down the Option key on your keyboard to see where the two touch points would lie, and carefully reposition them so that things work correctly. :]

In fact, even on a device you might find this a difficult feat if you have fairly large fingers. I found that the best way to get two cells selected was to try pinching not with thumb and forefinger, but with fingers from two different hands.

These are just teething issues that you can feel free to fix by increasing the height of the cells, for instance. And increasing the height of cells is as simple as changing the SHC_ROW_HEIGHT define.

The next step is to handle the pinch and part the list. Open SHCTableViewPinchToAdd.m and add the following to the end of handlePinch::

    if (recognizer.state == UIGestureRecognizerStateChanged
        && _pinchInProgress
        && recognizer.numberOfTouches == 2) {
        [self pinchChanged:recognizer];
    }

This code checks the _pinchInProgress instance variable that was set to YES in pinchStarted:. So this method will be executed only if the touch points are on two neighboring items, since otherwise _pinchInProgress would not be set to YES.

Now add the pinchChanged: method that the above code depends upon:

-(void)pinchChanged:(UIPinchGestureRecognizer *)recognizer {
    // find the touch points
    SHCTouchPoints currentTouchPoints = [self getNormalisedTouchPoints:recognizer];
 
    // determine by how much each touch point has changed, and take the minimum delta
    float upperDelta = currentTouchPoints.upper.y - _initialTouchPoints.upper.y;
    float lowerDelta = _initialTouchPoints.lower.y - currentTouchPoints.lower.y;
    float delta = -MIN(0, MIN(upperDelta, lowerDelta));
 
    // offset the cells, negative for the cells above, positive for those below
    NSArray* visibleCells = _tableView.visibleCells;
    for (int i=0; i < visibleCells.count; i++) {
        UIView* cell = (UIView*)visibleCells[i];
        if (i <= _pointOneCellindex) {
            cell.transform = CGAffineTransformMakeTranslation(0,  -delta);
        }
        if (i >= _pointTwoCellindex) {
            cell.transform = CGAffineTransformMakeTranslation(0, delta);
        }
    }
}

The implementation for pinchChanged: determines the delta, i.e., by how much the user has moved their finger, then applies a transform to each cell in the list: positive for items below the parting, and negative for those above.

In the first and second parts of this tutorial series, you moved cells by changing their frame, whereas in the above code, you apply a transform instead. Using a transform has the big advantage that it is easy to move a cell back to its original location: you simply “zero” the translation (i.e., apply the identity), instead of having to store the original frame for each and every cell that is moved.

Build, run, and have fun parting the list!

As the list parts, you want to scale the placeholder so that it appears to “spring out” from between the two items that are being parted. Add the following to the end of pinchChanged:

    // scale the placeholder cell
    float gapSize = delta * 2;
    float cappedGapSize = MIN(gapSize, SHC_ROW_HEIGHT);
    _placeholderCell.transform = CGAffineTransformMakeScale(1.0f, cappedGapSize / SHC_ROW_HEIGHT );
 
    _placeholderCell.label.text = gapSize > SHC_ROW_HEIGHT ?
              @"Release to Add Item" : @"Pull to Add Item";
 
    _placeholderCell.alpha = MIN(1.0f, gapSize / SHC_ROW_HEIGHT);
 
    // determine whether they have pinched far enough
    _pinchExceededRequiredDistance = gapSize > SHC_ROW_HEIGHT;

The scale transform, combined with a change in alpha, creates quite a pleasing effect:

You can probably turn off that purple highlight now. :]

You might have noticed the instance variable, pinchExceededRequiredDistance, which is set at the end of pinchChanged:. This records whether the user has “parted” the list by more than the height of one row. In this case, when the user finishes the pinch gesture, you need to add a new item to the list.

But before finishing the gesture code, you need to extend the table datasource to allow insertion of items at any index. Open SHCTableViewDataSource.h and add the following method prototype:

// Informs the datasource that a new item has been added at the given index
-(void) itemAddedAtIndex:(NSInteger)index;

The application view controller already has an implementation for itemAdded, which adds an item at the top of the list. This implementation can be repurposed to use the same logic for inserting an item mid-list.

Open SHCViewController.m and replace itemAdded with the following:

-(void)itemAdded {
    [self itemAddedAtIndex:0];
}
 
-(void)itemAddedAtIndex:(NSInteger)index {
    // create the new item
    SHCToDoItem* toDoItem = [[SHCToDoItem alloc] init];
    [_toDoItems insertObject:toDoItem atIndex:index];
 
    // refresh the table
    [_tableView reloadData];
 
    // enter edit mode
    SHCTableViewCell* editCell;
    for (SHCTableViewCell* cell in _tableView.visibleCells) {
        if (cell.todoItem == toDoItem) {
            editCell = cell;
            break;
        }
    }
    [editCell.label becomeFirstResponder];
}

As before, as soon as an item is inserted into the list, it is immediately editable.

Now, back to the interaction logic! Open SHCTableViewPinchToAdd.m and add the following code to the end of handlePinch: to handle the end of the pinch:

    if (recognizer.state == UIGestureRecognizerStateEnded) {
        [self pinchEnded:recognizer];
    }

Now add the following method:

-(void)pinchEnded:(UIPinchGestureRecognizer *)recognizer {
    _pinchInProgress = false;
 
    // remove the placeholder cell
    _placeholderCell.transform = CGAffineTransformIdentity;
    [_placeholderCell removeFromSuperview];
 
    if (_pinchExceededRequiredDistance) {
        // add a new item
        int indexOffset = floor(_tableView.scrollView.contentOffset.y / SHC_ROW_HEIGHT);
        [_tableView.dataSource itemAddedAtIndex:_pointTwoCellindex + indexOffset];
    } else {
        // Otherwise animate back to position
        [UIView animateWithDuration:0.2f
                              delay:0.0f
                            options:UIViewAnimationOptionCurveEaseInOut
                         animations:^{
                             NSArray* visibleCells = _tableView.visibleCells;
                             for(SHCTableViewCell* cell in visibleCells)
                             {
                                 cell.transform = CGAffineTransformIdentity;
                             }
                         }
                         completion:nil];
    }
 
}

This method performs two different functions. First, if the user has pinched further than the height of a to-do item, the datasource method you just added is invoked.

Otherwise, the list closes the gap between the two items. This is achieved using a simple animation. Earlier, when you coded the item-deleted animation, you used the completion block to re-render the entire table. With this gesture, the animation returns all of the cells back to their original positions, so it’s not necessary to redraw the entire table.

And with that, your app is finally done. Build, run, and enjoy your completed to-do list with gesture-support!

Separation of Concerns

Before you put down your tools, I want to make one more observation about the code. In the first part of this tutorial, you refactored the pull-to-add code so that it was no longer a subclass of the table view class. This allows the interaction to be added (or removed) via one simple line of code:

_dragAddNew = [[SHCTableViewDragAddNew alloc] initWithTableView:self.tableView];

The same is true of the pinch-to-add interaction.

Unfortunately, neither solution is quite as elegant as it might first seem. The SHCTableViewDragAddNew class sets itself as the delegate for the table view, and as a result, you cannot add any other interactive behaviors to the table that rely on receiving messages from the delegate.

What you really need here is some form of multicasting.

Image courtesy of kainet, used under Creative Commons ShareAlike license

As an open question to all readers, have you had this problem in the past? Have you ever wanted to add multiple classes as a delegate for a UI control? If so, I’d love to hear about it, because I have a solution in mind…

Where To Go From Here?

I hope you have enjoyed this tutorial and are inspired to think about how to make better use of gestures in your own apps. Resist the urge to rely on buttons, sliders, and other tired old user interface metaphors. Think about how you can allow your users to interact using natural gestures.

To my mind the key word here is natural. All of the gestures that you have added to this to-do list feel natural, because they result in a user interface that reacts to your touch in much the same way that real objects do. This is one of the most compelling features of a touch-based interface!

If you do use gestures, bear in mind that they might not be as discoverable as a more blatant “Click to Add New” button. Think about how you can improve their discoverability via contextual cues.

In this example, the cues have all been visual, but they don’t have to be! Why not try using sounds or vibration? But please, do so in moderation.

If you want to develop this to-do app further, why not try adding a reorder function, where a tap-and-hold gesture floats an item above the list, allowing it to be dragged around. Again, think about the physics of this interaction. The item being dragged should appear larger and cast a shadow over the other items in the list.

You can find the source code for the completed app on GitHub. If you do develop it further, why not fork the project?

Enjoy creating gesture-driven interfaces, and please share you stories and successes in the forum discussion below! :]


This is a post by Tutorial Team Member Colin Eberhardt, CTO of ShinobiControls, creators of playful and powerful iOS controls. Check out their app, ShinobiPlay.

Colin Eberhardt

Colin Eberhardt has been writing code and tutorials for many years, covering a wide range of technologies and platforms. Most recently he has turned his attention to iOS. Colin is CTO of ShinobiControls, creators of charts, grids and other powerful iOS controls.

You can check out their app, ShinobiPlay, in the App Store.

User Comments

25 Comments

[ 1 , 2 ]
  • Has anyone successfully been able to add reordering to the UITableView using a long press gesture? I'm hitting brick walls here, as soon as the table goes into edit mode you get the standard reorder image on the cells.

    I'm guessing that you have to calculate the position of the drag over the cells and perform an animation of the one above and below the current position? Any pointers wecomed!
    Bluey
  • Hello, nice tutorial, I have been looking for this type of tutorial for ages. I am trying to incorporate this example into a storyboard application with a navigation controller. I am getting this error, [UITableView registerClassforCells:]: unrecognized selector sent to instance. Also the custom tableview isn't showing up in the custom class identity inspector in the storyboard. Any help or ideas will be appreciated.
    Many thanks

    stephen.
    healey7
  • Hello,
    I'm little bit disappointed. When I start with first part of this tutorial, UITableView don't draw background for cells correctly when user scroll fast. That's why you decide write own TableView. But the problem is there too and scrolling is moreover slower, not so smooth like with UITableView from Apple. And finished app is very buggy ...
    I appreciate your work and I take some good points from this tutorial, but still I don't know, how to repair problem with scrolling. Any ideas here?

    Thank you.
    Deny
  • Hi, first i would like to say thanks this great tutorial series. i was reading a coment made from KyleFrost writing the toDoItems into a plist and i have the same problem. Im fairly new in xcode and ive tried many different variable but i still cant find a way to save the nsMutableArray. I would be greatly appreciate it If you could point me in the right direction.

    Thanks in advance and again Great Work!

    Toni
    nino1toni
  • Great tutorial ,

    I would like to know how to add a Tab Bar controller to this app

    thanks in advance
    shadynasser
  • Colin in your project there is bug with -reloadData. Two days I try solve this problem, but i can't do it. I describe problem there: http://stackoverflow.com/questions/1925 ... -clear-app
    plz help find solution
    middi
  • What a great learning experience!!
    Glenn
  • Can we somehow make the edit mode appear smoother? When the user selects a cell to edit, the animation seems quite choppy? I couldn't imagine this being a performance issue, so why is the frames per second so low?
    mathiasppc
  • First of all, thanks for the awesome tutorial!

    Would you happen to know why the animation when the user taps a cell to edit is choppy, and what we could do to make it appear smoother? Is this a performance issue?
    mathiasppc
  • Hello Nice tutorial , may i know how to add new item if all the items are deleted. I am not able to drag it if i remove all the items.
    Bhavika
[ 1 , 2 ]

Other Items of Interest

Ray's Monthly Newsletter

Sign up to receive a monthly newsletter with my favorite dev links, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

Vote for Our Next Tutorial!

Every week, we alternate between Gaming and Non-Gaming tutorial votes. This week: Non-Gaming!

    Loading ... Loading ...

Last week's winner: How To Make a Tower Defense Game with Swift.

Suggest a Tutorial - Past Results

Hang Out With Us!

Every month, we have a free live Tech Talk - come hang out with us!


Coming up in December: The Great CALayer Tour

Sign Up - December

Our Books

Our Team

Tutorial Team

  • Kirill Muzykov
  • Matt Luedke

... 59 total!

Update Team

  • Ray Fix

... 14 total!

Editorial Team

  • John Clem

... 22 total!

Code Team

  • Orta Therox

... 3 total!

Subject Matter Experts

  • Richard Casey

... 4 total!