How To Make a Gesture-Driven To-Do List App Like Clear in Swift: Part 1/2

Audrey Tam
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!

Update 04/21/2015: Updated for Xcode 6.3 and Swift 1.2.

Update note: This tutorial was fully updated for iOS 8 and Swift by Audrey Tam. Original post by Tutorial Team member Colin Eberhardt. Updated December 9 2014 for Xcode 6.1.1.

This two-part tutorial series will take you through the development of a simple to-do list app 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 creating a custom table view cell. 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.


To help users understand a visual computer interface, we design UIs to look like physical objects that the user is already familiar with. For years, we designed buttons on an iPhone screen to look like buttons in the physical world because users already know how to push buttons. Until the release of iOS 7 in the fall of 2013, Apple 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. With iOS 7, Apple moved drastically away from skeuomorphism, removing many of the shadows and borders reminiscent of the physical world. In addition, the user experience was built around gestures more than ever before: swipe up from the bottom of the screen to reveal Control Center replaced the home button double tap. Swipe right from the left edge is encouraged over the Back button. As Apple embraces gestures in their user experience, it’s time to consider how to use gestures in our own apps.

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!


There are several steps in this tutorial, so it might be helpful to see the overall plan before you begin. You’ll be completing 7 checkpoints:

  1. You’ll start by creating a basic UITableView with a UIViewController named ViewController that conforms to UITableViewDataSource protocol, displaying a hard-coded list of ToDoItems.
  2. Then you’ll set ViewController to conform to UITableViewDelegate protocol, and implement UITableViewDelegate methods to modulate row color from red to yellow.
  3. To fine-tune the “look”, you’ll create a custom UITableViewCell named TableViewCell, where you’ll implement a gradient effect within each table row.
  4. The first gesture you’ll implement is swipe-left-to-delete, and the first step is to add a UIPanGestureRecognizer to TableViewCell, and detect this delete gesture.
  5. TableViewCell needs to delegate the actual deletion from ToDoItems, so you’ll create a protocol named TableViewCellDelegate for ViewController to adopt
  6. Next, you’ll handle swipe-right-to-complete, which layers a strikethrough over the row text, and also changes the row color to green: you’ll implement this by writing a custom UILabel named StrikeThroughText.
  7. As a final polish, you’ll implement TableViewCell properties crossLabel and tickLabel, to display contextual cues while the user is swiping left or right. They’ll change color (red cross for delete, green tick for complete) when the user has dragged the row far enough left or right.

Getting Started

This first Checkpoint is just a basic table of to-do items so, if you’re already comfortable with doing that, download this starter project and skip down to Styling Your Cells

If you want some practice, 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:


The standard approach to rendering scrollable lists within an iPhone application is to use a UITableView, so you’ll add a Table View to the storyboard view and connect it to an outlet in the view controller that was created in the project template.

Open Main.storyboard and drag a Table View onto the scene (view), positioning it so that it covers the whole scene:


To ensure tableView always takes up the entire screen, pin its top, bottom, left and right edges to be 0 points from the parent view.

Next, connect the Table View to an outlet in ViewController.swift: show the Assistant Editor, select the Table View, control-drag from it into ViewController.swift, just inside the class block, and name the outlet tableView:


Delete the didReceiveMemoryWarning method.

Note:The eagle-eyed among you might be wondering why I used a UITableView within a UIViewController – why not use a UITableViewController? Well, I originally wrote this with a UITableViewController and everything worked fine until the very last step of Part 2, when it became unstable. The version I wrote with a UIViewController still worked well, so it’s possible there’s a subtle bug in Swift.

A to-do list is essentially a list of items rendered on the screen, so you need to create an object that represents each to-do item. In the Project Navigator, right-click ViewController.swift and select New File…, then select the iOS\Source\Cocoa Touch Class template and add a class called ToDoItem. Make it a subclass of NSObject, set Language to Swift, but don’t create a XIB file:


Open ToDoItem.swift and add two properties and an init method (between the curly braces { }):

// A text description of this item.
var text: String
// A Boolean value that determines the completed state of this item.
var completed: Bool
// Returns a ToDoItem initialized with the given text and default completed value. 
init(text: String) {
    self.text = text
    self.completed = false

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

Note: Selecting a file in the Project Navigator before you create a New File causes Xcode to place the new file just below the file that you selected.

Open ViewController.swift and, in viewDidLoad, declare and initialize the toDoItems array:

class ViewController: UIViewController {
    @IBOutlet weak var tableView: UITableView!
    var toDoItems = [ToDoItem]()
    override func viewDidLoad() {
        if toDoItems.count > 0 {
        toDoItems.append(ToDoItem(text: "feed the cat"))
        toDoItems.append(ToDoItem(text: "buy eggs"))
        toDoItems.append(ToDoItem(text: "watch WWDC videos"))
        toDoItems.append(ToDoItem(text: "rule the Web"))
        toDoItems.append(ToDoItem(text: "buy a new iPhone"))
        toDoItems.append(ToDoItem(text: "darn holes in socks"))
        toDoItems.append(ToDoItem(text: "write this tutorial"))
        toDoItems.append(ToDoItem(text: "master Swift"))
        toDoItems.append(ToDoItem(text: "learn to draw"))
        toDoItems.append(ToDoItem(text: "get more exercise"))
        toDoItems.append(ToDoItem(text: "catch up with Mom"))
        toDoItems.append(ToDoItem(text: "get a hair cut"))

Next, add the required UITableViewDataSource methods:

// MARK: - Table view data source
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return 1
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    return toDoItems.count
func tableView(tableView: UITableView,
    cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCellWithIdentifier("cell", 
            forIndexPath: indexPath) as! UITableViewCell
        let item = toDoItems[indexPath.row]
        cell.textLabel?.text = item.text
        return cell

Then declare that ViewController conforms to UITableViewDataSource and UITableViewDelegate protocols:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate {

And finish configuring tableView in viewDidLoad by adding these lines just below super.viewDidLoad:

tableView.dataSource = self
tableView.delegate = self
tableView.registerClass(UITableViewCell.self, forCellReuseIdentifier: "cell")

And with that, Checkpoint 1 is done! Set the active scheme to iPhone 5, 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, the next two steps make the list a little bit easier on the eyes. :]

You’ll use color to separate the table rows so, in ViewController‘s viewDidLoad, set the tableView‘s separator style to None. While you’re there, make the rows a little bigger to increase readability:

tableView.separatorStyle = .None
tableView.rowHeight = 50.0

Note: If you are planning to support versions of iOS prior to iOS 8, you may also need to implement heightForRowAtIndexPath. Simply returning rowHeight will be sufficient, as in the code below:

func tableView(tableView: UITableView, heightForRowAtIndexPath 
    indexPath: NSIndexPath) -> CGFloat {
    return tableView.rowHeight;

The UIViewController class also conforms to UITableViewDelegate. Add the code below to the end of ViewController.swift to set the background color of each row, adding slightly more green as you go:

// MARK: - Table view delegate
func colorForIndex(index: Int) -> UIColor {
    let itemCount = toDoItems.count - 1
    let val = (CGFloat(index) / CGFloat(itemCount)) * 0.6
    return UIColor(red: 1.0, green: val, blue: 0.0, alpha: 1.0)
func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, 
                        forRowAtIndexPath indexPath: NSIndexPath) {
    cell.backgroundColor = colorForIndex(indexPath.row)

The color returned by colorForIndex(index:) creates a gradient effect from red to yellow, just for aesthetic purposes. Build and run the app again to see that you’ve completed Checkpoint 2:


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

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\Source\Cocoa Touch Class template. Name the class TableViewCell and make it a subclass of UITableViewCell. Make sure you uncheck the option to create a XIB file and set the Language to Swift.


Replace the contents of TableViewCell.swift with the following:

import UIKit
import QuartzCore
class TableViewCell: UITableViewCell {
    let gradientLayer = CAGradientLayer()
    required init(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        // gradient layer for cell
        gradientLayer.frame = bounds
        let color1 = UIColor(white: 1.0, alpha: 0.2).CGColor as CGColorRef
        let color2 = UIColor(white: 1.0, alpha: 0.1).CGColor as CGColorRef
        let color3 = UIColor.clearColor().CGColor as CGColorRef
        let color4 = UIColor(white: 0.0, alpha: 0.1).CGColor as CGColorRef
        gradientLayer.colors = [color1, color2, color3, color4]
        gradientLayer.locations = [0.0, 0.01, 0.95, 1.0]
        layer.insertSublayer(gradientLayer, atIndex: 0)
    override func layoutSubviews() {
        gradientLayer.frame = bounds

Here you add a CAGradientLayer property 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, 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.

Now you need to switch over to using your new custom UITableView cell in your code! Only two steps are required:

Step 1: In ViewController.swift‘s viewDidLoad, change the cell class from UITableViewCell to TableViewCell:

tableView.registerClass(TableViewCell.self, forCellReuseIdentifier: "cell")

This tells the tableView to use the TableViewCell class whenever it needs a cell with reuse identifier “cell”.

Step 2: Change the cell class cast in cellForRowAtIndexPath to TableViewCell (and make sure the label’s background color is clear, so the gradient shines through), as follows:

let cell = tableView.dequeueReusableCellWithIdentifier("cell", forIndexPath: indexPath) 
    as TableViewCell
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 check off Checkpoint 3: your to-do items should now have a subtle gradient, making it much easier to differentiate between individual rows:



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.

Two small tweaks in ViewController.swift before you really get stuck into this: add this line to viewDidLoad, just after you set tableView.separatorStyle:

tableView.backgroundColor = UIColor.blackColor()

This makes the tableView black, under the cell you’re dragging.

And add this line in cellForRowAtIndexPath, after the line that creates the cell:

cell.selectionStyle = .None

This gets rid of the highlighting that happens when you select a table cell.

Open TableViewCell.swift and add the following code at the end of the overridden init method:

// add a pan recognizer
var recognizer = UIPanGestureRecognizer(target: self, action: "handlePan:")
recognizer.delegate = self

This code adds a pan gesture recognizer to your custom table view cell, and sets the cell itself as the recognizer’s delegate. Any pan events will be sent to handlePan but, before adding that method, you need to set up two properties that it will use.

Add these two properties at the top of TableViewCell.swift, right below the existing gradientLayer property:

var originalCenter = CGPoint()
var deleteOnDragRelease = false

Now add the implementation for handlePan, at the end of TableViewCell.swift:

//MARK: - horizontal pan gesture methods
func handlePan(recognizer: UIPanGestureRecognizer) {
  // 1
  if recognizer.state == .Began {
    // when the gesture begins, record the current center location
    originalCenter = center
  // 2
  if recognizer.state == .Changed {
    let translation = recognizer.translationInView(self)
    center = CGPointMake(originalCenter.x + translation.x, originalCenter.y)
    // has the user dragged the item far enough to initiate a delete/complete?
    deleteOnDragRelease = frame.origin.x < -frame.size.width / 2.0
  // 3
  if recognizer.state == .Ended {
    // the frame this cell had before user dragged it
    let originalFrame = CGRect(x: 0, y: frame.origin.y,
        width: bounds.size.width, height: 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 go through 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 property 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).

Next, give the recognizer’s delegate (the table view cell) something to do by adding this UIGestureRecognizerDelegate method, below handlePan:

override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool {
    if let panGestureRecognizer = gestureRecognizer as? UIPanGestureRecognizer {
        let translation = panGestureRecognizer.translationInView(superview!)
        if fabs(translation.x) > fabs(translation.y) {
            return true
        return false
    return false

This delegate method allows you to cancel the recognition of 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 the gesture recognizer, 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 halfway across the screen to the left, indicating that the item should be deleted:


Note: If you find you have to drag an item much more than halfway across to the left, to get it to stick, you need to set constraints on the Table View, to stop it from overflowing the device’s window – in Main.storyboard, select the View Controller then click the Resolve Auto Layout Issues button and select All Views in View Controller\Reset to Suggested Constraints. This will make the Table View fit into the device’s window.


And that’s Checkpoint 4 done! Of course, you’ll notice that the cell just gets stuck and doesn’t actually disappear, and if you scroll it off the screen then back again, the item is still there – to complete this swipe-to-delete gesture, you need to remove the item from your list and reload the table.

The to-do items are stored in an Array 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.

To add a new protocol to the project, add this code to TableViewCell.swift, below the import statements but above the class TableViewCell block:

// A protocol that the TableViewCell uses to inform its delegate of state change
protocol TableViewCellDelegate {
    // indicates that the given item has been deleted
    func toDoItemDeleted(todoItem: ToDoItem)

This code defines a protocol with a required method that indicates an item has been deleted.

The TableViewCell class needs to expose this delegate, and it also needs to know which model item (i.e., ToDoItem) it is rendering, so that it can pass this item to its delegate.

Add these two properties near the top of TableViewCell.swift, just below the definitions of originalCenter and deleteOnDragRelease:

// The object that acts as delegate for this cell.
var delegate: TableViewCellDelegate?
// The item that this cell renders.
var toDoItem: ToDoItem?

You declare these properties as optionals, because you’ll set their values in ViewController.swift, not in TableViewCell‘s init method.

In order to use this delegate, update the logic for handlePan in TableViewCell.swift by adding the following code to the end of the if recognizer.state == .Ended block:

if deleteOnDragRelease {
    if delegate != nil && toDoItem != nil {
        // notify the delegate that this item should be deleted

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 ViewController.swift and, just above the // MARK: - Table view delegate line, add an implementation for the TableViewCellDelegate method toDoItemDeleted, to delete an item when notified:

func toDoItemDeleted(toDoItem: ToDoItem) {
  let index = (toDoItems as NSArray).indexOfObject(toDoItem)
  if index == NSNotFound { return }
  // could removeAtIndex in the loop but keep it here for when indexOfObject works
  // use the UITableView to animate the removal of this row
  let indexPathForRow = NSIndexPath(forRow: index, inSection: 0)
  tableView.deleteRowsAtIndexPaths([indexPathForRow], withRowAnimation: .Fade)

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

Next, scroll up to the top of ViewController.swift and declare that this class conforms to your new protocol:

class ViewController: UIViewController, UITableViewDataSource, UITableViewDelegate, TableViewCellDelegate {

Finally, add the following lines to the end of cellForRowAtIndexPath (right before the return statement) to set the TableViewCell‘s delegate and toDoItem properties:

cell.delegate = self
cell.toDoItem = item

Build and run your project, and delete some items, to check off Checkpoint 5:


Only two more Checkpoints to go! This next one is pretty long and a bit intense, with a small DIY exercise, so feel free to take a short coffee/tea break before we continue…


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. There are a few implementations of a UILabel with a strikethrough effect on 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 you’re going to create a custom UILabel with a strikeThroughLayer and a strikeThrough flag, add this custom label to your custom cell and, in handlePan, set strikeThrough to true if the user drags the cell more than halfway to the right.

First, create a New File with the iOS\Source\Cocoa Touch Class template. Name the class StrikeThroughText, and make it a subclass of UILabel, with Language set to Swift.

Open StrikeThroughText.swift and replace its contents with the following:

import UIKit
import QuartzCore
// A UILabel subclass that can optionally have a strikethrough.
class StrikeThroughText: UILabel {
    let strikeThroughLayer: CALayer
    // A Boolean value that determines whether the label should have a strikethrough.
    var strikeThrough : Bool {
        didSet {
            strikeThroughLayer.hidden = !strikeThrough
            if strikeThrough {
    required init(coder aDecoder: NSCoder) {
        fatalError("NSCoding not supported")
    override init(frame: CGRect) {
        strikeThroughLayer = CALayer()
        strikeThroughLayer.backgroundColor = UIColor.whiteColor().CGColor
        strikeThroughLayer.hidden = true
        strikeThrough = false
        super.init(frame: frame)
    override func layoutSubviews() {
    let kStrikeOutThickness: CGFloat = 2.0
    func resizeStrikeThrough() {
        let textSize = text!.sizeWithAttributes([NSFontAttributeName:font])
        strikeThroughLayer.frame = CGRect(x: 0, y: bounds.size.height/2, 
                           width: textSize.width, height: kStrikeOutThickness)

strikeThroughLayer is basically a white layer that is re-positioned (by resizeStrikeThrough) according to the size of the rendered text. This layer is hidden if strikeThrough is false, and visible if strikeThrough is true. The resizeStrikeThrough method is called when strikeThrough gets set to true.

Note: Follow the next instructions carefully with regards to their order and position within the init method. You are about to create a property that is not optional. Remember that in Swift, properties that are not optional must be initialized before calling super.init.

OK, so you have your strikethrough label, but it needs to be added to your custom cell. Do that by opening TableViewCell.swift and adding two properties right below the property for deleteOnDragRelease:

let label: StrikeThroughText
var itemCompleteLayer = CALayer()

Initialize label (and get rid of the red error flag!) by adding the following code at the top of the init method, before the call to super.init:

// create a label that renders the to-do item text
label = StrikeThroughText(frame: CGRect.nullRect)
label.textColor = UIColor.whiteColor()
label.font = UIFont.boldSystemFontOfSize(16)
label.backgroundColor = UIColor.clearColor()

Configure the cell by adding these lines after the call to super.init:

// remove the default blue highlight for selected cells
selectionStyle = .None

Still in init, add the following code right before you add the pan recognizer:

// add a layer that renders a green background when an item is complete
itemCompleteLayer = CALayer(layer: layer)
itemCompleteLayer.backgroundColor = UIColor(red: 0.0, green: 0.6, blue: 0.0, 
                                            alpha: 1.0).CGColor
itemCompleteLayer.hidden = true
layer.insertSublayer(itemCompleteLayer, atIndex: 0)

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

Now replace the existing code for layoutSubviews with the following, to layout your new itemCompleteLayer and label:

let kLabelLeftMargin: CGFloat = 15.0
override func layoutSubviews() {
    // ensure the gradient layer occupies the full bounds
    gradientLayer.frame = bounds
    itemCompleteLayer.frame = bounds
    label.frame = CGRect(x: kLabelLeftMargin, y: 0, 
                         width: bounds.size.width - kLabelLeftMargin, 
                         height: bounds.size.height)   

Next, add the following didSet observer for the todoItem property. This will ensure the strikethrough label stays in sync with the toDoItem.

var toDoItem: ToDoItem? {
    didSet {
        label.text = toDoItem!.text
        label.strikeThrough = toDoItem!.completed
        itemCompleteLayer.hidden = !label.strikeThrough

Now that you’ve set the label’s text within the didSet observer, you no longer need to set it in cellForRowAtIndexPath. Open ViewController.swift and comment out that line of code:

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

In fact, if you don’t comment out or delete this line, you’ll see an unpleasant shadowing from the doubling up of the text.

StrikeThroughText also takes care of the background color, so you can comment out the line in cellForRowAtIndexPath that sets the textLabel’s background color:

//cell.textLabel?.backgroundColor = UIColor.clearColor()

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!




Did you get it working? If not, take a peek:

Solution Inside: Completed Item Solution SelectShow

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:


You’ve finished Checkpoint 6 and 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 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 or cross that is 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 two UILabel properties to TableViewCell.swift, just below deleteOnDragRelease:

var tickLabel: UILabel!, crossLabel: UILabel!

Declare these as unwrapped optionals, as you can’t create them until after the call to super.init (because you’ll be calling a new instance method createCueLabel to create them).

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

let kUICuesMargin: CGFloat = 10.0, kUICuesWidth: CGFloat = 50.0

Now add the createCueLabel method for creating the cue labels, just below the layoutSubviews method:

// utility method for creating the contextual cues
func createCueLabel() -> UILabel {
    let label = UILabel(frame: CGRect.nullRect)
    label.textColor = UIColor.whiteColor()
    label.font = UIFont.boldSystemFontOfSize(32.0)
    label.backgroundColor = UIColor.clearColor()
    return label

And initialize your new cue labels in init by adding the following code right before the itemCompleteLayer lines:

// tick and cross labels for context cues
tickLabel = createCueLabel()
tickLabel.text = "\u{2713}"
tickLabel.textAlignment = .Right
crossLabel = createCueLabel()
crossLabel.text = "\u{2717}"
crossLabel.textAlignment = .Left

Well, that’s how I wrote this code the first time around, but something didn’t feel quite right … and then I remembered that Swift lets you nest functions! So I moved some lines around, to initialize tickLabel and crossLabel before calling super.init, and got rid of those pesky exclamation marks! Can you figure out what to do without peeking?

Solution Inside: No Unwrapped Optionals Here! SelectShow

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 = CGRect(x: -kUICuesWidth - kUICuesMargin, y: 0, 
                         width: kUICuesWidth, height: bounds.size.height)
crossLabel.frame = CGRect(x: bounds.size.width + kUICuesMargin, y: 0, 
                          width: kUICuesWidth, height: 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, at the end of the if recognizer.state == .Changed block, in order to adjust the alpha of the labels as the user drags the cell:

// fade the contextual clues
let cueAlpha = fabs(frame.origin.x) / (frame.size.width / 2.0)
tickLabel.alpha = cueAlpha
crossLabel.alpha = cueAlpha
// indicate when the user has pulled the item far enough to invoke the given action
tickLabel.textColor = completeOnDragRelease ? 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’ve finished Checkpoint 7 and the first part of this two-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 usable 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 check out 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, please join the forum discussion below!

Audrey Tam

Audrey Tam retired at the end of 2012 from a 25-year career as a computer science academic. Her teaching included Pascal, C/C++, Java, Java web services, web app development in php and mysql, user interface design and evaluation, and iOS programming. Before moving to Australia, she worked on Fortran and PL/1 simulation software at IBM's development lab in Silicon Valley. Audrey now teaches short courses in iOS app development to non-programmers, and attends nearly all Melbourne Cocoaheads monthly meetings.

Other Items of Interest

Save time.
Learn more with our video courses. Weekly

Sign up to receive the latest tutorials from each week, and receive a free epic-length tutorial as a bonus!

Advertise with Us!

PragmaConf 2016 Come check out Alt U

Our Books

Our Team

Video Team

... 19 total!

Swift Team

... 15 total!

iOS Team

... 33 total!

Android Team

... 15 total!

macOS Team

... 10 total!

Apple Game Frameworks Team

... 11 total!

Unity Team

... 11 total!

Articles Team

... 12 total!

Resident Authors Team

... 15 total!