How to Make a Gesture-Driven To-Do List App Like Clear: Part 1/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 three-part tutorial series will take you through the development of a simple to-do list application that is free from buttons, toggle switches and other common user interface (UI) controls.

Instead, users will interact with your app via a set of intuitive gestures, including swipes, pull-to-add, and pinch. In eschewing the common interface components, you’ll present the user with a more striking and clutter-free interface. It’s not an empty gesture!

This tutorial is for intermediate or advanced developers – you will be doing some tricky things like working with gradient layers, performing animations, and even creating a custom table view. If you are a beginner developer, you should start with some of our other tutorials.

If you want to make better use of gestures in your application, then this is the tutorial for you. Read on to start the hand aerobics!

Skeuomorphism and Touch Interfaces

Before diving into the code, it’s worth taking some time to discuss the role of gestures in UI design. Don’t worry – it’s a “gripping” topic!

The mobile multi-touch interface allows for much more direct interaction – and therefore much more control and expression – than does a simple mouse pointer device.

Some very cool and intuitive gestures have been developed, such as pinch/stretch, flick, pan, and tap-and-hold. But they are rarely used! (One notable exception is the pinch/stretch, which has become the standard mechanism for manipulating images.)

Despite the expressive nature of touch, we developers still fall back on the same old UI paradigms of buttons, sliders, and toggle switches. Why?

One of the reasons we continue to use these same-old UI components is due to a design philosophy known as skeuomorphism.

Ragecomic

To help users understand a visual computer interface, we design UIs to look like physical objects that the user is already familiar with. Apple has thoroughly embraced skeuomorphic design in its own applications, achieving almost photo-realistic representations of physical objects, such as notebooks and bookshelves.

But hey – designs can evolve as readily as technology. Graphical computer interfaces have been around for 40 years. Isn’t it time we ask ourselves, “Are buttons really necessary?”

I thoroughly recommending watching Josh Clarke’s presentation “Buttons are a Hack”, wherein he encourages developers to think more creatively about gestures and touch interactions. The next time you go to add a new control to your interface, ask yourself, “Can I perform the same function via touch?”

When an application comes along that makes good use of gestures, it is quite striking. A recent example is
Clear by Realmac software. Be sure to check out the great demo on YouTube, or even better download the app to check it out.

This tutorial describes the development of a to-do list application that is very similar to Clear. The purpose of this tutorial is to encourage you to think about how to make better use of gestures in your own applications, rather than to create a clone of Clear. I encourage you to download and buy Clear, as it is a truly inspirational app.

Anyhow, I think it’s time I climbed down from my soapbox and showed you all some code!

Getting Started

Fire up Xcode and create a new iPhone application by going to File\New\Project, selecting the iOS\Application\Single View Application template and tapping Next. On the next screen, enter ClearStyle as the product name, and fill in the other details similar to the image below:

Note that you’ll use Automatic Reference Counting (ARC), but not Storyboards, as this is a single-page application. Also note that a Class Prefix is set here – you can omit that, but if you do, be aware that the auto-generated names for some files will be different from what’s specified in this tutorial.

A to-do list is essentially a list of items rendered on the screen. The standard approach to rendering scrollable lists within an iPhone application is to use a UITableView. So you’ll next add one of these to the view controller that was created as part of the project template.

Click on SHCViewController.xib in order to open the Interface Builder and drag a table view onto the view surface:

In order to access the UITableView, you have to add a referencing outlet. So, bring up the Assistant Editor (tap on the middle button in the Editor group of buttons on the top-right of the Xcode toolbar) and control-drag from the table view onto SHCViewController.h, as shown below. Name the outlet tableView:

And with that, your minimalist user interface is complete!

The eagle-eyed among you might be wondering why I used a UITableView within a UIViewController – why not use a UITableViewController? Without giving too much away, let me just say that in the next part of this tutorial, you’ll be replacing the UITableView with your own custom implementation. So there’s a method to my madness. :]

To render a list of to-dos, you need to create an object that represents each to-do item. So let’s do that!

Right-click the project root in the Project Navigator and select New File…, then select the iOS\Cocoa Touch\Objective-C class template and add a class called SHCToDoItem. Make it a subclass of NSObject:

Open SHCToDoItem.h and add a couple of properties and methods (between the @interface and @end lines):

// A text description of this item.
@property (nonatomic, copy) NSString *text;
 
// A Boolean value that determines the completed state of this item.
@property (nonatomic) BOOL completed;
 
// Returns an SHCToDoItem item initialized with the given text. 
-(id)initWithText:(NSString*)text;
 
// Returns an SHCToDoItem item initialized with the given text.  
+(id)toDoItemWithText:(NSString*)text;

A to-do item is simply a string of text and a Boolean that indicates whether the item is complete or not.

If you’ve followed some of the older tutorials on this site, you might expect to synthesize the properties you just added. But surprise! You don’t have to do that any longer with Xcode 4.5 (and you are using Xcode 4.5, aren’t you?), since the compiler will automatically synthesize properties for you. Handy, eh?

You next need to add the implementation for initWithText:, which initializes a SHCToDoItem instance with the supplied text. For convenience, also add the toDoItemWithText: class method, which makes object creation easier.

Add the following code to SHCToDoItem.m after the @implementation line:

-(id)initWithText:(NSString*)text {
    if (self = [super init]) {
      self.text = text;
    }
    return self;
}
 
+(id)toDoItemWithText:(NSString *)text {
    return [[SHCToDoItem alloc] initWithText:text];
}

Now that you have your to-do item class, creating an array of to-do items and displaying them in the UITableView is pretty standard stuff, so we’ll quickly rattle through the next few steps.

Add the following code to the top of SHCViewController.m (below the existing #import line, replacing the empty class extension and the @implementation line):

#import "SHCToDoItem.h"
 
@implementation SHCViewController {
    // an array of to-do items
    NSMutableArray* _toDoItems;
}
 
-(id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
    if (self)
    {
        // create a dummy to-do list
        _toDoItems = [[NSMutableArray alloc] init];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Feed the cat"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Buy eggs"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Pack bags for WWDC"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Rule the web"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Buy a new iPhone"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Find missing socks"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Write a new tutorial"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Master Objective-C"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Remember your wedding anniversary!"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Drink less beer"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Learn to draw"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Take the car to the garage"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Sell things on eBay"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Learn to juggle"]];
        [_toDoItems addObject:[SHCToDoItem toDoItemWithText:@"Give up"]];
    }
    return self;
}

Here you import the new to-do item class header, add an instance variable _toDoItems, and override initWithNibName:bundle: to populate this array with dummy data. The above code shows the value of the toDoItemWithText: class method, which has removed the need to repeatedly alloc/init the to-do objects.

You need to supply a datasource for the table view. For a simple application, it makes sense to use the view controller as the datasource, so go right ahead and edit SHCViewController.h to adopt the UITableViewDataSource protocol:

@interface SHCViewController : UIViewController <UITableViewDataSource>

You need to set the view controller as the datasource of the table view. Normally, you would do this via Interface Builder by connecting the datasource for the table view to the SHCViewController. But you can also do this via code by adding the following line to the end of viewDidLoad in SHCViewController.m:

self.tableView.dataSource = self;
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];

The above code also registers the UITableViewCell class as the class that will supply cells for the table view.

The UITableViewDataSource protocol defines two methods that must be implemented by any class that adopts the protocol. One (tableView:numberOfRowsInSection:) details the number of rows in each section, and the other (tableView:cellForRowAtIndexPath:) requests cell instances for a specific row/section.

The implementation of these methods is pretty simple – just add the code shown below to the end of SHCViewController.m (but before the closing @end):

#pragma mark - UITableViewDataSource protocol methods
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return _toDoItems.count;
}
 
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSString *ident = @"cell";
    // re-use or create a cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ident forIndexPath:indexPath];
    // find the to-do item for this index
    int index = [indexPath row];
    SHCToDoItem *item = _toDoItems[index];
    // set the text
    cell.textLabel.text = item.text;
    return cell;
}

You only have a single section, which is the default behavior for the table view, so the above simply returns the number of items in response to the tableView:numberOfRowsInSection: message.

The implementation for tableView:cellForRowAtIndexPath: is mostly boilerplate code: a cell is created, the relevant to-do item is retrieved, and the text on the cell is set.

Note: In iOS versions before iOS 6, when creating a new UITableViewCell, you had to first dequeue the cell, and if you didn’t get a cell via the reusable pool, you had to create the cell explicitly via code. This is no longer necessary in iOS 6, since dequeueReusableCellWithIdentifier:forIndexPath: automatically creates a new cell for you if one isn’t available via the reuse pool, as long as you have a class registered for the cell identifier beforehand. (That’s what you did in viewDidLoad above.)

Build and run your code, and you will be presented with the wonderfully minimalist to-do list shown below:

Styling Your Cells

Before you start adding gestures, let’s make the list a little bit easier on the eyes. :]

The UITableView class has a separate protocol definition that is used for styling UITableViewDelegate. Switch to SHCViewController.h and add this protocol to the list:

@interface SHCViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>

And of course, the table view delegate has to be set either via Interface Builder or code. Do it in code by adding the following to the end of viewDidLoad in SHCViewController.m:

    self.tableView.delegate = self;
 
    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
    self.tableView.backgroundColor = [UIColor blackColor];

The code also removes the separators and changes the background color for the table view.

You can now add the code below to the end of the file to increase the height of each row and to set the background color per row:

-(UIColor*)colorForIndex:(NSInteger) index {
    NSUInteger itemCount = _toDoItems.count - 1;
    float val = ((float)index / (float)itemCount) * 0.6;
    return [UIColor colorWithRed: 1.0 green:val blue: 0.0 alpha:1.0];
}
 
#pragma mark - UITableViewDataDelegate protocol methods
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 50.0f;
}
 
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    cell.backgroundColor = [self colorForIndex:indexPath.row];
}

The color returned by colorForIndex creates a gradient effect from red to yellow, just for aesthetic purposes. Build and run the app again to see this in action:

The current implementation sets a specific color for each row. While the overall effect is a gradient color change as the user scrolls down, notice that it’s hard to tell where one cell begins and another ends, especially towards the top, where most of the cells have a red background.

So the next step is to add a gradient effect to each cell (i.e., row) so that it’s easier to tell the cells apart. You could easily modify the cell’s appearance in the datasource or delegate methods that you have already implemented, but a much more elegant solution is to subclass UITableViewCell and customize the cell directly.

Add a new class to the project with the iOS\Cocoa Touch\Objective-C class template. Name the class SHCTableViewCell, and make it a subclass of UITableViewCell:

Replace the contents of SHCTableViewCell.m with the following:

#import <QuartzCore/QuartzCore.h>
#import "SHCTableViewCell.h"
 
@implementation SHCTableViewCell
{
    CAGradientLayer* _gradientLayer;       
}
 
-(id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // add a layer that overlays the cell adding a subtle gradient effect
        _gradientLayer = [CAGradientLayer layer];
        _gradientLayer.frame = self.bounds;
        _gradientLayer.colors = @[(id)[[UIColor colorWithWhite:1.0f alpha:0.2f] CGColor],
                                (id)[[UIColor colorWithWhite:1.0f alpha:0.1f] CGColor],
                                (id)[[UIColor clearColor] CGColor],
                                (id)[[UIColor colorWithWhite:0.0f alpha:0.1f] CGColor]];
        _gradientLayer.locations = @[@0.00f, @0.01f, @0.95f, @1.00f];
        [self.layer insertSublayer:_gradientLayer atIndex:0];
    }
    return self;
}
 
-(void) layoutSubviews {
    [super layoutSubviews];
    // ensure the gradient layers occupies the full bounds
    _gradientLayer.frame = self.bounds;
}
 
@end

Here you add a CAGradientLayer instance variable and create a four-step gradient within the init method. Notice that the gradient is a transparent white at the very top, and a transparent black at the very bottom. This will be overlaid on top of the existing color background, to cause the effect of lightening the top and darkening the bottom, to create a neat bevel effect simulating a light source shining down from the top.

Note: Still trying to get your head wrapped around how to properly shade user interfaces and other graphics to simulate lighting? Check out this lighting tutorial by Vicki.

Also notice that layoutSubviews has been overridden. This is to ensure that the newly-added gradient layer always occupies the full bounds of the frame.

Try compiling this code, and you will find that the compiler reports a couple of linker errors. This is because the above code uses the QuartzCore framework.

To keep the compiler happy, click on the project root to bring up the project settings page, then expand the Link Binary With Libraries section of the Build Phases tab, and click the plus (+) button that allows you to add frameworks to your project. You should find QuartzCore on the list.

That’s the framework done, but you’re still not using your new custom UITableView cell in your code! You need to switch over to using the custom cell before you can see your new code in action.

Switch to SHCViewController.m and add the following import line at the top:

#import "SHCTableViewCell.h"

Then, replace the following line in viewDidLoad:

	[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];

With this:

	[self.tableView registerClass:[SHCTableViewCell class] forCellReuseIdentifier:@"cell"];

Finally, change the cell class in tableView:cellForRowAtIndexPath: to SHCTableCellClass, as follows (and make sure the label’s background is clear) as follows:

    SHCTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ident forIndexPath:indexPath];
    cell.textLabel.backgroundColor = [UIColor clearColor];

That’s it! Since you register the class to be used to create a new table view cell in viewDidLoad, when tableView:cellForRowAtIndexPath: next needs a table cell, your new class will be used automatically. :]

Build and run your app, and your to-do items should now have a subtle gradient, making it much easier to differentiate between individual rows:

Swipe-to-Delete

Now that your list is presentable, it’s time to add your first gesture. This is an exciting moment!

Multi-touch devices provide app developers with complex and detailed information regarding user interactions. As each finger is placed on the screen, its position is tracked and reported to your app as a series of touch events. Mapping these low-level touch events to higher-level gestures, such as pan or a pinch, is quite challenging.

A finger is not exactly the most accurate pointing device! And as a result, gestures need to have a built-in tolerance. For example, a user’s finger has to move a certain distance before a gesture is considered a pan.

Fortunately, the iOS framework provides a set of gesture recognizers that has this all covered. These handy little classes manage the low-level touch events, saving you from the complex task of identifying the type of gesture, and allowing you to focus on the higher-level task of responding to each gesture.

This tutorial will skip over the details, but if you want to learn more check out our UIGestureRecognizer tutorial.

Add a pan gesture recognizer to your custom table view cell by adding the following code to the end of init (within the if condition) in SHCTableViewCell.m:

// add a pan recognizer
UIGestureRecognizer* recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handlePan:)];
recognizer.delegate = self;
[self addGestureRecognizer:recognizer];

Any pan events will be sent to handlePan:, but before adding that method, you need a couple of instance variables. Add the following at the top of the file, right below the existing _gradientLayer instance variable:

	CGPoint _originalCenter;
	BOOL _deleteOnDragRelease;

Now add the method to the end of the file:

#pragma mark - horizontal pan gesture methods
-(BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer {
    CGPoint translation = [gestureRecognizer translationInView:[self superview]];
    // Check for horizontal gesture
    if (fabsf(translation.x) > fabsf(translation.y)) {
        return YES;
    }
    return NO;
}
 
-(void)handlePan:(UIPanGestureRecognizer *)recognizer {   
    // 1
    if (recognizer.state == UIGestureRecognizerStateBegan) {
         // if the gesture has just started, record the current centre location
        _originalCenter = self.center;
    }
 
    // 2
    if (recognizer.state == UIGestureRecognizerStateChanged) {
        // translate the center
        CGPoint translation = [recognizer translationInView:self];
        self.center = CGPointMake(_originalCenter.x + translation.x, _originalCenter.y);
        // determine whether the item has been dragged far enough to initiate a delete / complete
        _deleteOnDragRelease = self.frame.origin.x < -self.frame.size.width / 2;
 
    }
 
    // 3
    if (recognizer.state == UIGestureRecognizerStateEnded) {
        // the frame this cell would have had before being dragged
        CGRect originalFrame = CGRectMake(0, self.frame.origin.y,
                                          self.bounds.size.width, self.bounds.size.height);
        if (!_deleteOnDragRelease) {
            // if the item is not being deleted, snap back to the original location
            [UIView animateWithDuration:0.2
                             animations:^{
                                 self.frame = originalFrame;
                             }
             ];
        }
    }
}

There’s a fair bit going on in this code. Let’s start with handlePan:, section by section.

  1. Gesture handlers, such as this method, are invoked at various points within the gesture lifecycle: the start, change (i.e., when a gesture is in progress), and end. When the pan first starts, the center location of the cell is recorded in _originalCenter.
  2. As the pan gesture progresses (as the user moves their finger), the method determines the offset that should be applied to the cell (to show the cell being dragged) by getting the new location based on the gesture, and offsetting the center property accordingly. If the offset is greater than half the width of the cell, you consider this to be a delete operation. The _deleteOnDragRelease instance variable acts as a flag that indicates whether or not the operation is a delete.
  3. And of course, when the gesture ends, you check the flag to see if the action was a delete or not (the user might have dragged the cell more than halfway and then dragged it back, effectively nullifying the delete operation).

Then there’s gestureRecognizerShouldBegin – what does that do? You might have noticed that as well as providing handlePan: as the action for the gesture, the code above also indicates that the cell class is being used as the delegate for the pan gesture.

This method allows you to cancel a gesture before it has begun. In this case, you determine whether the pan that is about to be initiated is horizontal or vertical. If it is vertical you cancel it, since you don’t want to handle any vertical pans.

This is a very important step! Your cells are hosted within a vertically scrolling view. Failure to cancel a vertical pan renders the scroll view inoperable, and the to-do list will no longer scroll.

Build and run this code, and you should find that you can now drag the items left or right. When you release, the item snaps back to the center, unless you drag it more than half way across the screen to the left, indicating that the item should be deleted:

Of course, you’ll also notice that the item doesn’t actually get deleted. :] So how do you remove an item from your list?

The to-do items are stored in an NSMutableArray within your view controller. So you need to find some way to signal to the view controller that an item has been deleted and should be removed from this array.

UI controls use protocols to indicate state change and user interactions. You can adopt the same approach here.

Add a new protocol to the project with the iOS\Cocoa Touch\Objective-C protocol template. Name it SHCTableViewCellDelegate.

Now open SHCTableViewCellDelegate.h and replace its contents with:

#import "SHCToDoItem.h"
 
// A protocol that the SHCTableViewCell uses to inform of state change
@protocol SHCTableViewCellDelegate <NSObject>
 
// indicates that the given item has been deleted
-(void) toDoItemDeleted:(SHCToDoItem*)todoItem;
 
@end

The above code adds a single method that indicates an item has been deleted.

The custom cell class needs to expose this delegate, but it also needs to know which model item (i.e., SHCToDoItem) it is rendering. Replace the contents of SHCTableViewCell.h with the following to add this information:

#import "SHCToDoItem.h"
#import "SHCTableViewCellDelegate.h"
 
// A custom table cell that renders SHCToDoItem items.
@interface SHCTableViewCell : UITableViewCell
 
// The item that this cell renders.
@property (nonatomic) SHCToDoItem *todoItem;
 
// The object that acts as delegate for this cell. 
@property (nonatomic, assign) id<SHCTableViewCellDelegate> delegate;
 
@end

In order to use this delegate, update the logic for handlePan: in SHCTableViewCell.h by adding the following code to the end of the last if block (the one checking whether the gesture state is ended):

    if (_deleteOnDragRelease) {
        // notify the delegate that this item should be deleted
        [self.delegate toDoItemDeleted:self.todoItem];
    }

The above code invokes the delegate method if the user has dragged the item far enough.

Now it’s time to make use of the above changes. Switch to SHCViewController.h and declare the class as supporting the new protocol (and also add the necessary #import):

#import "SHCTableViewCellDelegate.h"
 
@interface SHCViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, SHCTableViewCellDelegate>

Then, open SHCViewController.m and add the following line to the end of tableView:cellForRowAtIndex: (right before the return statement):

cell.delegate = self;
cell.todoItem = item;

Finally, add an implementation for the newly added delegate method to delete an item when necessary:

-(void)toDoItemDeleted:(id)todoItem {
    // use the UITableView to animate the removal of this row
    NSUInteger index = [_toDoItems indexOfObject:todoItem];
    [self.tableView beginUpdates];
    [_toDoItems removeObject:todoItem];
    [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]]
                          withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView endUpdates];    
}

The above code removes the to-do item, and then uses the UITableView to animate the deletion, using one of its stock effects.

Swipe-to-Complete

Your to-do list application allows the user to delete items, but what about marking them as complete? For this, you’ll use a swipe-right gesture.

When an item is marked as complete, it should be rendered with a green background and strikethrough text. Unfortunately, iOS does not support strikethrough text rendering, so you are going to have to implement this yourself!

I’ve found a few implementations of a UILabel with a strikethrough effect via StackOverflow, but all of them use drawRect and Quartz 2D to draw the strikethrough. I much prefer using layers for this sort of thing, since they make the code easier to read, and the layers can be conveniently turned on and off via their hidden property.

Note: Alternatively, you can do this with the new NSAttributedString functionality in iOS 6. For more information, check out Chapter 15 in iOS 6 by Tutorials, “What’s New with Attributed Strings.”

So, create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class SHCStrikethroughLabel, and make it a subclass of UILabel.

Open SHCStrikethroughLabel.h and replace its contents with the following:

// A UILabel subclass that can optionally have a strikethrough.
@interface SHCStrikethroughLabel : UILabel
 
// A Boolean value that determines whether the label should have a strikethrough.
@property (nonatomic) bool strikethrough;
 
@end

Switch to SHCStrikethroughLabel.m and replace its contents with the following:

#import <QuartzCore/QuartzCore.h>
#import "SHCStrikethroughLabel.h"
 
@implementation SHCStrikethroughLabel {
    bool _strikethrough;
    CALayer* _strikethroughLayer;
}
 
const float STRIKEOUT_THICKNESS = 2.0f;
 
-(id)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (self) {
        _strikethroughLayer = [CALayer layer];
        _strikethroughLayer.backgroundColor = [[UIColor whiteColor] CGColor];
        _strikethroughLayer.hidden = YES;
        [self.layer addSublayer:_strikethroughLayer];
    }
    return self;
}
 
-(void)layoutSubviews {
    [super layoutSubviews];
    [self resizeStrikeThrough];
}
 
-(void)setText:(NSString *)text {
    [super setText:text];
    [self resizeStrikeThrough];
}
 
// resizes the strikethrough layer to match the current label text
-(void)resizeStrikeThrough {
    CGSize textSize = [self.text sizeWithFont:self.font];
    _strikethroughLayer.frame = CGRectMake(0, self.bounds.size.height/2,
                                           textSize.width, STRIKEOUT_THICKNESS);
}
 
#pragma mark - property setter
-(void)setStrikethrough:(bool)strikethrough {
    _strikethrough = strikethrough;
    _strikethroughLayer.hidden = !strikethrough;
}
 
@end

The strikethrough is basically a white layer that is re-positioned according to the size of the rendered text. Note that as the strikethrough property is set, the strikethrough layer is shown or hidden as necessary.

OK, so you have your strikethrough label, but it needs to be added to your custom cell. Do that by opening SHCTableViewCell.m and adding an import for the new class:

#import "SHCStrikethroughLabel.h"

Then add a couple of instance variables right below the instance variable for _deleteOnDragRelease:

	SHCStrikethroughLabel *_label;
	CALayer *_itemCompleteLayer;

Next, add the following code to the top of initWithStyle:reuseIdentifier: (right after if (self)):

    // create a label that renders the to-do item text
    _label = [[SHCStrikethroughLabel alloc] initWithFrame:CGRectNull];
    _label.textColor = [UIColor whiteColor];
    _label.font = [UIFont boldSystemFontOfSize:16];
    _label.backgroundColor = [UIColor clearColor];
    [self addSubview:_label];
    // remove the default blue highlight for selected cells
    self.selectionStyle = UITableViewCellSelectionStyleNone;

Still in initWithStyle:reuseIdentifier:, add the following code right before you add the gesture recognizer:

    // add a layer that renders a green background when an item is complete
    _itemCompleteLayer = [CALayer layer];
    _itemCompleteLayer.backgroundColor = [[[UIColor alloc] initWithRed:0.0 green:0.6 blue:0.0 alpha:1.0] CGColor];
    _itemCompleteLayer.hidden = YES;
    [self.layer insertSublayer:_itemCompleteLayer atIndex:0];

The above code adds to your custom cell both the strikethrough label and a solid green layer that will be shown when an item is complete.

Now replace the existing code for layoutSubviews with the following:

const float LABEL_LEFT_MARGIN = 15.0f;
 
-(void)layoutSubviews {
    [super layoutSubviews];
    // ensure the gradient layers occupies the full bounds
    _gradientLayer.frame = self.bounds;
    _itemCompleteLayer.frame = self.bounds;
    _label.frame = CGRectMake(LABEL_LEFT_MARGIN, 0,
                              self.bounds.size.width - LABEL_LEFT_MARGIN,self.bounds.size.height);   
}

Also add the following setter for the todoItem property:

-(void)setTodoItem:(SHCToDoItem *)todoItem {
    _todoItem = todoItem;
    // we must update all the visual state associated with the model item
    _label.text = todoItem.text;
    _label.strikethrough = todoItem.completed;
    _itemCompleteLayer.hidden = !todoItem.completed;
}

Now that you’re setting the label’s text within the setter, open SHCViewController.m and comment out this line of code that used to set the label explicitly:

//cell.textLabel.text = item.text;

The final thing you need to do is detect when the cell is dragged more than halfway to the right, and set the completed property on the to-do item. This is pretty similar to handling the deletion – so would you like to try that on your own? You would? OK, I’ll wait for you to give it a shot, go ahead!

…waiting…

…waiting…

…waiting…

Tomato-San is angry!

Tomato-San is angry!

Did you even try?! Get to it, I’ll wait here! :]

…waiting…

…waiting…

…waiting…

Did you get it working? If not, let’s review.

You start off by adding a new instance variable to SHCTableViewCell.m, which will act as a flag indicating whether or not the item is complete:

	BOOL _markCompleteOnDragRelease;

Next, in the UIGestureRecognizerStateChanged block in handlePan:, you set the flag depending on how far right the cell was dragged, as follows (you can add the code right above the line where _deleteOnDragRelease is set):

        _markCompleteOnDragRelease = self.frame.origin.x > self.frame.size.width / 2;

Finally, still in handlePan: but now in the UIGestureRecognizerStateEnded block, you mark the cell as complete if the completion flag is set (add the code to the very end of the if block):

        if (_markCompleteOnDragRelease) {
            // mark the item as complete and update the UI state
            self.todoItem.completed = YES;
            _itemCompleteLayer.hidden = NO;
            _label.strikethrough = YES;
        }

As you’ll notice, the code marks the item as complete, shows the completion layer (so that the cell will have a green background) and enables the strikethrough effect on the label.

All done! Now you can swipe items to complete or delete. The newly added green layer sits behind your gradient layer, so that the completed rows still have that subtle shading effect.

Build and run, and it should look something like this:

It’s starting to look sweet!

Contextual Cues

The to-do list now has a novel, clutter-free interface that is easy to use… once you know how. One small problem with gesture-based interfaces is that their functions are not as immediately obvious to the end user, as opposed to their more classic skeuomorphic counterparts.

One thing you can do to aid a user’s understanding of a gesture-based interface, without compromising on simplicity, is to add contextual cues. For a great article on contextual cues, I recommend reading this blog post by my friend Graham Odds, which includes a number of examples.

Contextual cues often communicate functionality and behavior to the user by reacting to the user’s movements. For example, the mouse pointer on a desktop browser changes as the user moves their mouse over a hyperlink.

The same idea can be used on a touch- or gesture-based interface. When a user starts to interact with the interface, you can provide subtle visual cues that encourage further interaction and indicate the function that their gesture will invoke.

For your to-do app, a simple tick and cross that are revealed as the user pulls an item left or right will serve to indicate how to delete or mark an item as complete. So go right ahead and add them!

Add a couple of UILabel instance variables to SHCTableViewCell.m, as follows:

	UILabel *_tickLabel;
	UILabel *_crossLabel;

Next, define a couple of constant values (that you’ll use soon) just above initWithStyle:reuseIdentifier::

const float UI_CUES_MARGIN = 10.0f;
const float UI_CUES_WIDTH = 50.0f;

Now initialize the labels in initWithStyle:reuseIdentifier: by adding the following code right after the if (self) line:

        // add a tick and cross
        _tickLabel = [self createCueLabel];
        _tickLabel.text = @"\u2713";
        _tickLabel.textAlignment = NSTextAlignmentRight;
        [self addSubview:_tickLabel];
        _crossLabel = [self createCueLabel];
        _crossLabel.text = @"\u2717";
        _crossLabel.textAlignment = NSTextAlignmentLeft;
        [self addSubview:_crossLabel];

And add the following method to create the labels:

// utility method for creating the contextual cues
-(UILabel*) createCueLabel {
    UILabel* label = [[UILabel alloc] initWithFrame:CGRectNull];
    label.textColor = [UIColor whiteColor];
    label.font = [UIFont boldSystemFontOfSize:32.0];
    label.backgroundColor = [UIColor clearColor];
    return label;
}

Rather than using image resources for the tick and cross icons, the above code uses a couple of Unicode characters. You could probably find some better images for this purpose, but these characters give us a quick and easy way of implementing this effect, without adding the overhead of images.

Note: Wondering how I knew these unicode values represented a checkmark and a cross mark? Check out this handy list of useful Unicode symbols!

Now, add the following code to the end of layoutSubviews to relocate these labels:

    _tickLabel.frame = CGRectMake(-UI_CUES_WIDTH - UI_CUES_MARGIN, 0,
                                  UI_CUES_WIDTH, self.bounds.size.height);
    _crossLabel.frame = CGRectMake(self.bounds.size.width + UI_CUES_MARGIN, 0,
                                   UI_CUES_WIDTH, self.bounds.size.height);

The above code positions the labels off screen, the tick to the left and the cross to the right.

Finally, add the code below to handlePan:, to the end of the if block that detects the UIGestureRecognizerStateChanged state, in order to adjust the alpha of the labels as the user drags the cell:

// fade the contextual cues
float cueAlpha = fabsf(self.frame.origin.x) / (self.frame.size.width / 2);
_tickLabel.alpha = cueAlpha;
_crossLabel.alpha = cueAlpha;
 
// indicate when the item have been pulled far enough to invoke the given action
_tickLabel.textColor = _markCompleteOnDragRelease ?
        [UIColor greenColor] : [UIColor whiteColor];
_crossLabel.textColor = _deleteOnDragRelease ?
        [UIColor redColor] : [UIColor whiteColor];

The cue is further reinforced by changing the color of the tick/cross to indicate when the user has dragged the item far enough – as you’ll notice when you build and run the app again:

And with that final feature, you are done with the first part of this three-part series!

Where To Go From Here?

Here’s an example project containing all the source code from this part of the series.

What next? So far, the app only allows the user to mark items as complete or to delete them. There are clearly more gestures and features that need to be added in order to make this a fully useable application.

However, I am not too keen on the “stock” delete animation provided by UITableView. I’m sure this could be done in a slightly more eye-catching way.

Unfortunately, there is a limit to how much you can extend the UITableView, which is why part two of this series replaces this control with your own custom implementation. But since it’s a fairly large topic, you’ll have to wait for part two to find out all about it. :]

In the meantime, why not think about your own applications and how you can replace the existing controls with more interesting and natural gestures. Also, if you do use gestures, don’t forget to think about how to help your users discover them, and the possibility of using contextual cues.

And if you have any questions or comments about what you’ve done so far, be sure to let me know in the forums!


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

45 Comments

[ 1 , 2 , 3 ]
  • Hi and thanks a lot for this tutorial.

    I'm using iOS7 SDK and Xcode5 (dp5). I Can't get the contextual clues working, they don't appear on the sides of the UITableViewCell... I even tried your final project on Github and it still doesn't work.
    It appears we can't draw outside of cell's bounds, even if I set its clipToBounds property to NO...

    Any idea ...?

    Thanks in advance.

    Arnaud (France)
    ArnaudNe
  • I am working with iOS 7 SDK and there are some things that are not working like the clues and the change of background. I goint to learn more about this issues to fixed in iOS 7 but for now is a great tutorial and I am learning a lot. Thanks
    Gidrek
  • I am also having issues with having this to work with iOS 7. Everything works fine except the cell gets resized to the table width. Any ideas?
    sero323
  • Hi. I am using Xcode 5 and when I try to run my app in the iOS Simulator after the 'getting started' steps the app opens but then it's just a black screen. Any help will be appreciated. Thanks.
    lukecoburn
  • Hi,

    Use [self.contentView.superview setClipsToBounds:NO]; in initWithStyle function.

    Its solve your problem in Xcode 5 and in iOS 7 SDK
    aanchal.baheti
  • I am very green with iOS development and Objective-C so please bear with me, but I am stuck at the very beginning before even styling the cells. Did anyone else have this issue? The list items are not populating the tableView when I run the program in the simulator.

    I am using Xcode 5, so is there any version changes that could have occurred between the version when the tutorial was made and now with Xcode 5?
    missmagdalene
  • missmagdalene wrote:I am very green with iOS development and Objective-C so please bear with me, but I am stuck at the very beginning before even styling the cells. Did anyone else have this issue? The list items are not populating the tableView when I run the program in the simulator.

    I am using Xcode 5, so is there any version changes that could have occurred between the version when the tutorial was made and now with Xcode 5?


    I also am very green! But I managed to get it to work by placing the array code into the "viewDidLoad" section. There's probably a better way to do it... =P
    mafew
  • I'm pretty new to all of this...

    I'm using Xcode 5 with IOS7, I'm at the point where I've checked off the tasks successfully, but the green background colour slides OVER the stricken text. I feel like this is a layer issue but I have no idea how to solve this... ???

    Screen Shot 2013-12-06 at 5.22.28 PM.png
    mafew
  • There seems to be a problem with the contextual cues labels on iOS 7. The labels are hidden if the frame of the labels is outside of the cell it self. How can this be solved?
    osklar0328
  • If you add following under if(self) { you're gonna be just fine. Great stackoverflow question on this issue helped me out (http://stackoverflow.com/questions/1886 ... iew-bounds)

    self.clipsToBounds = NO;
    self.contentView.clipsToBounds = NO;
    if ([self.contentView.superview isKindOfClass:[NSClassFromString(@"UITableViewCellScrollView") class]]) self.contentView.superview.clipsToBounds = NO;
    emilc
  • I have a question about SHCToDoItem class methods. Why can't we just do the following? What's the meaning of this additional class?

    [_toDoItems addObject:@"Feed the cat"];


    EDIT:

    Never mind, dumb question, I understand why now.
    emilc
  • mafew wrote:I'm pretty new to all of this...

    I'm using Xcode 5 with IOS7, I'm at the point where I've checked off the tasks successfully, but the green background colour slides OVER the stricken text. I feel like this is a layer issue but I have no idea how to solve this... ???

    Screen Shot 2013-12-06 at 5.22.28 PM.png


    You have to add following under [self addSubview:_label];

    // Add _label layer above completeLayer
    [self.layer insertSublayer:_label.layer atIndex:2];
    emilc
  • Forgot to thank you for this awesome tutorial you've got here! Great work!
    emilc
  • Hi,

    How to disable right to left gesture?

    Thanks in advance
    jcamachov
[ 1 , 2 , 3 ]

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!

Hang Out With Us!

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


Coming up in July: Facebook Pop Tech Talk!

Sign Up - July

RWDevCon Conference?

We are considering having an official raywenderlich.com conference called RWDevCon in DC in early 2015.

The conference would be focused on high quality Swift/iOS 8 technical content, and connecting as a community.

Would this be something you'd be interested in?

    Loading ... Loading ...

Our Books

Our Team

Tutorial Team

... 50 total!

Update Team

  • Riccardo D'Antoni

Editorial Team

  • Ryan Nystrom

... 23 total!

Code Team

  • Orta Therox

... 1 total!

Translation Team

... 33 total!

Subject Matter Experts

  • Richard Casey

... 4 total!