How to Make a Gesture-Driven To-Do List App Like Clear: Part 3/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 is the last in a three-part tutorial series that walks you through creating a simple to-do list app free of buttons, […] By Colin Eberhardt.

Leave a rating/review
Save for later
Share

Contents

Hide contents

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!

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 and Twitter

This is the last in a three-part tutorial series that walks you through creating a simple to-do list app free of buttons, toggle switches and other common user interface (UI) controls, opting for a purely gesture-driven approach that vastly simplifies your UI.

If you followed the first and second parts of this series, you should now have a to-do list application that supports swipe gestures for marking items as complete and for deleting them (accompanied by a funky “shuffle” animation). Your app also allows the user to edit items.

But there is one glaring omission in the app’s functionality – the user cannot add new items to the list! Of course, I’m not sure that’s such a bad thing – I hate adding new to-dos to my never-ending list. :]

A conventional approach to this problem would most likely be to add a button with the text “Add new” on a title bar. But remember to ask yourself every time you want to add a new UI control: can I perform the same function via a gesture?

I’m guessing that you know the answer in this case, as in most cases, is YES. Read on to learn how!

The Pull-to-Add Gesture

The gestures that feel the most natural tend to play on the illusion that the phone UI is a physical object that obeys the same laws of physics as the natural world. Deleting an item from the to-do list by “pulling” it off the side of the screen feels quite natural, in the same way that you might swiftly pull a straw out in a game of KerPlunk.

The pull-down gesture has become ubiquitous in mobile apps as a means to refresh a list. The pull-down gesture feels very much like you are pulling against the natural resistance of the list, as if it were a hanging rope, in order to physically pull more items in from the top. Again, it is a natural gesture that in some way reflects how things work in the “real” world.

There has been some concern about the legality of using the pull-to-refresh gesture, due to a user interface patent. However, the recent introduction of this feature in the iOS email application (with a gorgeous tear-drop effect), the iOS 6 SDK itself, and its popularity in the App Store means that developers are less concerned about this (ridiculous) patent.

Note: To learn more about iOS 6’s built-in pull-to-refresh control, check out Chapter 20 in iOS 6 by Tutorials, “What’s New with Cocoa Touch.”

Pulling down on the list to add a new item at the top is a great gesture to add to your to-do list application, so in this part of the tutorial, you’ll start with that!

You could add the new logic to your custom table implementation, SHCTableView. However, as more functionality is added, that class runs the risk of getting crowded, and the code hard to follow.

A better approach is to add the code that implements the pull-to-add gesture into a separate class. This code requires access to the scroll view delegate methods, so a suitable approach is to subclass SHCTableView.

Start by adding a new class to the project. Create a new file with the iOS\Cocoa Touch\Objective-C class template. Name the class SHCTableViewDragAddNew, and make it a subclass of SHCTableView.

The UIScrollViewDelegate, which SHCTableView already adopts (and which your newly-added SHCTableViewDragAddNew also supports, by virtue of the fact that it is a subclass), has methods that allow you to perform actions as the user scrolls the list.

In order to implement a pull-to-add gesture, you first have to detect when the user has started to scroll while at the top of the list. Then, as the user pulls further down, position a placeholder element that indicates where the new item will be added.

SHCTableViewCell, which renders each item in the list, can be used as the placeholder. Do that by replacing the contents of SHCTableViewDragAddNew.m with the following:

#import "SHCTableViewDragAddNew.h"
#import "SHCTableViewCell.h"

@implementation SHCTableViewDragAddNew {    
    // a cell that is rendered as a placeholder to indicate where a new item is added
    SHCTableViewCell* _placeholderCell;
}

-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _placeholderCell = [[SHCTableViewCell alloc] init];
        _placeholderCell.backgroundColor = [UIColor redColor];
    }
    return self;
}

@end

The above code simply sets up the instance variable for the placeholder and initializes it when an instance of SHCTableViewDragAddNew is instantiated.

In order to use SHCTableViewCell as a placeholder, you need access to the label that renders the text for the to-do item. This means exposing the label on the cell as a property.

So open SHCTableViewCell.h and add an import for SHCStrikethroughLabel at the top of the same file:

#import "SHCStrikethroughLabel.h"

Then, add the following property below the ones you added previously:

// the label used to render the to-do text
@property (nonatomic, strong, readonly) SHCStrikethroughLabel* label; 

Since you’re declaring the label as a property, you probably also want to remove the instance variable named _label that you have in SHCTableViewCell.m (it’s at the very beginning, along with the other instance variables).

Adding the placeholder when the pull gesture starts and maintaining its position is really quite straightforward. When dragging starts, check whether the user is currently at the start of the list, and if so, use a _pullDownInProgress instance variable to record this state.

Of course, you first have to add this new instance variable to SHCTableViewDragAddNew.m (it goes right below _placeholderCell):

    // indicates the state of this behavior
    BOOL _pullDownInProgress;

Now add the UIScrollViewDelegate method necessary to detect the beginning of a pull:

#pragma mark - UIScrollViewDelegate
-(void)scrollViewWillBeginDragging:(UIScrollView *)scrollView {
    // this behaviour starts when a user pulls down while at the top of the table
    _pullDownInProgress = scrollView.contentOffset.y <= 0.0f;
    if (_pullDownInProgress) {
        // add your placeholder
        [self insertSubview:_placeholderCell atIndex:0];
    }
}

While a scroll is in progress, the placeholder is repositioned by setting its frame using another UIScrollViewDelegate method.

The cell height for each row in the table view is currently hard-coded within SHCTableView.m. But you need access to this value within the SHCTableViewDragAddNew subclass. So switch to SHCTableView.m and locate the following line:

const float SHC_ROW_HEIGHT = 50.0f;

Remove that line from SHCTableView.h. Then, open SHCTableView.h and add the following line just above the @interface declaration:

#define SHC_ROW_HEIGHT 50.0f

Why did you change the value from a constant to a #define? Because SHCTableView is included from several other files (or will be soon), the compiler will start complaining about duplicate symbols if you have the constant value. You don't run into the same issue using the #define.

Actually, a better implementation might be to provide an optional delegate method that requests the row height, but this simple hard-coded approach will work for now.

Back in SHCTableViewDragAddNew.m, add an implementation for the scrollViewDidScroll: UIScrollViewDelegate method as follows:

-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [super scrollViewDidScroll:scrollView];
    
    if (_pullDownInProgress && self.scrollView.contentOffset.y <= 0.0f) {
        // maintain the location of the placeholder
        _placeholderCell.frame = CGRectMake(0, - self.scrollView.contentOffset.y - SHC_ROW_HEIGHT,
                                            self.frame.size.width, SHC_ROW_HEIGHT);
        _placeholderCell.label.text = -self.scrollView.contentOffset.y > SHC_ROW_HEIGHT ?
                @"Release to Add Item" : @"Pull to Add Item";
        _placeholderCell.alpha = MIN(1.0f, - self.scrollView.contentOffset.y / SHC_ROW_HEIGHT);
    } else {
        _pullDownInProgress = false;
    }
}

Note that since the superclass handles scrolling in order to recycle cells, the superclass implementation of scrollViewDidScroll: needs to be invoked. The rest of the code simply maintains the placeholder as the user scrolls.

When the user stops dragging, you need to check whether they pulled down far enough (i.e., by at least the height of a cell), and remove the placeholder. This is achieved by adding the implementation of scrollViewDidEndDragging::

-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    // check whether the user pulled down far enough
    if (_pullDownInProgress && - self.scrollView.contentOffset.y > SHC_ROW_HEIGHT) {
        // TODO – add a new item
    }
    _pullDownInProgress = false;
    [_placeholderCell removeFromSuperview];
}

As you'll notice, the code doesn't actually insert a new item yet. Later on, you’ll take a look at the logic required to update your array of model objects.

As you've seen, implementing a pull-down gesture is really quite easy! Did you notice the way that the above code adjusts the placeholder alpha and flips its text from “Pull to Add Item” to “Release to Add Item”? These are contextual cues, as mentioned in Part 1 of this series (you do remember, don’t you?).

Before you can test this code, you need to update the view controller to make use of SHCTableViewDragAddNew rather than SHCTableView. Open SHCViewController.xib, select the table view and, using the Identity Inspector, change the custom class to SHCTableViewDragAddNew.

You also need to change the type of the outlet in SHCViewController.h. First add a new import statement:

#import "SHCTableViewDragAddNew.h"

The header defines a single property, so next, change the type:

@property (weak, nonatomic) IBOutlet SHCTableViewDragAddNew *tableView;

Once this is done, build and run to see your new gesture in action:

When the drag gesture is completed, you need to add a new SHCToDoItem to the array holding the to-do items. The table currently exposes a datasource, and considering that this is a “data” operation rather than a cosmetic feature, the data source is the best place to add the required methods.

Open SHCTableViewDataSource.h and add the following method prototype (before the @end):

// Informs the datasource that a new item has been added at the top of the table
-(void)itemAdded;

Then open SHCTableViewDragAddNew.m and replace the “TODO” in scrollViewDidEndDragging: (which you added previously) with the following:

[self.dataSource itemAdded];

The SHCViewController already adopts the datasource protocol, so go right ahead and add an implementation for the new method. Open SHCViewController.m and add the following method:

-(void)itemAdded {
    // create the new item
    SHCToDoItem* toDoItem = [[SHCToDoItem alloc] init];
    [_toDoItems insertObject:toDoItem atIndex:0];
    // refresh the table
    [_tableView reloadData];
    // enter edit mode
    SHCTableViewCell* editCell;
    for (SHCTableViewCell* cell in _tableView.visibleCells) {
        if (cell.todoItem == toDoItem) {
            editCell = cell;
            break;
        }
    }
    [editCell.label becomeFirstResponder];
}

This code is pretty simple – it adds a new to-do item to the start of the array, and then forces an update of the table. Then it locates the cell that renders this newly added to-do item and sends a becomeFirstResponder: message to the text label in order to go straight into edit mode.

The end result is that as soon as a new item is added, the user can start entering the description for their to-do item:

That’s pretty slick!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.