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

Learn how to make a gesture-driven to-do list app like Clear, complete with table view tricks, swipes, and pinches. By Audrey Tam.

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

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
addSubview(tickLabel)
crossLabel = createCueLabel()
crossLabel.text = "\u{2717}"
crossLabel.textAlignment = .Left
addSubview(crossLabel)

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?

[spoiler title="No Unwrapped Optionals Here!"]
Move createCueLabel and its comment into init, just below the 4 lines for label, then move the 6 lines that begin tickLabel or crossLabel, along with their comment, up above the call to super.init, but below the createCueLabel function. Move the two addSubview lines to be with the other addSubview statement, just for tidiness, so init looks like this, just below the label configuration lines:

// 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
}
        
// 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
        
super.init(style: style, reuseIdentifier: reuseIdentifier)
        
addSubview(label)
addSubview(tickLabel)
addSubview(crossLabel)

and up in your property definitions, remove the exclamation marks from UILabel:

var tickLabel: UILabel, crossLabel: UILabel

My little bit of Swift-fu for the day :]
[/spoiler]

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:

Cues

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!

Contributors

Over 300 content creators. Join our team.