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 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. 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 {
                resizeStrikeThrough()
            }
        }
    }

    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)
        layer.addSublayer(strikeThroughLayer)
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        resizeStrikeThrough()
    }

    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:

addSubview(label)
// 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() {
    super.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!

...waiting...

...waiting...

...waiting...

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

[spoiler title="Completed Item Solution"]
You start off by adding a new property to TableViewCell.swift, which will act as a flag indicating whether or not the item is complete - you can put it on the same line as deleteOnDragRelease, to indicate that it has a similar purpose:

var deleteOnDragRelease = false, completeOnDragRelease = false

Next, in the if recognizer.state == .Changed 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 or below the line where deleteOnDragRelease is set):

completeOnDragRelease = frame.origin.x > frame.size.width / 2.0

Finally, still in handlePan but now in the if recognizer.state == .Ended block, you mark the cell as complete if the completion flag is set - tidy up the if-else logic while you're at it:

if recognizer.state == .Ended {
    let originalFrame = CGRect(x: 0, y: frame.origin.y, 
                               width: bounds.size.width, height: bounds.size.height)
    if deleteOnDragRelease {
        if delegate != nil && toDoItem != nil {
            // notify the delegate that this item should be deleted
            delegate!.toDoItemDeleted(toDoItem!)
        }
    } else if completeOnDragRelease {
        if toDoItem != nil {
            toDoItem!.completed = true
        }
        label.strikeThrough = true
        itemCompleteLayer.hidden = false
        UIView.animateWithDuration(0.2, animations: {self.frame = originalFrame})
    } else {
        UIView.animateWithDuration(0.2, animations: {self.frame = originalFrame})
    }
}

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

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:

Complete

You've finished Checkpoint 6 and it's starting to look sweet!

Contributors

Over 300 content creators. Join our team.