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

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 Google+ 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 […] By Colin Eberhardt.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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."

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!

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!

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!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.