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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
How To Make a Gesture-Driven To-Do List App Like Clear in Swift: Part 1/2
45 mins
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.”
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
.
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:
You've finished Checkpoint 6 and it's starting to look sweet!