How to Make a Gesture-Driven To-Do List App Like Clear: Part 2/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 second in a three-part tutorial series that takes you through developing a to-do list app that is completely free […] By Colin Eberhardt.

Leave a rating/review
Save for later
Share

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 second in a three-part tutorial series that takes you through developing a to-do list app that is completely free of buttons, toggle switches and other common, increasingly outdated user interface (UI) controls.

It’s nothing but swipes, pulls and pinches for this app! As I’m sure you’ve realized if you’ve been following along, that leaves a lot more room for content.

If you followed the first part of the tutorial, you should now have a stylish and minimalistic to-do list interface. Your users can mark items as complete by swiping them to the right, or delete them by swiping to the left.

Before moving on to adding more gestures to the app, this part of the tutorial will show you how to make a few improvements to the existing interactions.

Right now, the animation that accompanies a delete operation is a “stock” feature of UITableView – when an item is deleted, it fades away, while the items below move up to fill the space. This effect is a little jarring, and the animation a bit dull.

How about if instead, the deleted item continued its motion to the right, while the remaining items shuffled up to fill the space?

Ready to see how easy it can be to do one better than Apple’s stock gesture animations? Let’s get started!

A Funky Delete Animation

This part of the tutorial continues on from the previous one. If you did not follow Part 1, or just want to jump in at this stage, make sure you download the code from the first part, since you’ll be building on it in this tutorial.

Presently, the code for animating the deletion of a to-do item is as follows (the method is in SHCViewController.m):

-(void)toDoItemDeleted:(id)todoItem {
    // use the UITableView to animate the removal of this row
    NSUInteger index = [_toDoItems indexOfObject:todoItem];
    [self.tableView beginUpdates];
    [_toDoItems removeObject:todoItem];
    [self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:index inSection:0]]
                          withRowAnimation:UITableViewRowAnimationFade];
    [self.tableView endUpdates];    
} 

This uses the “stock” UITableViewRowAnimationFade effect, which is a bit boring! I’d much prefer the application to use a more eye-catching animation, where the items shuffle upwards to fill the space that was occupied by the deleted item.

The UITableView manages the lifecycle of your cells, so how do you manually animate their location? It’s surprisingly easy! UITableView includes the visibleCells method, which returns an array of all the cells that are currently visible. You can iterate over these items and do what you like with them!

So, let’s replace the stock animation with something a bit more exciting.

You’re going to use block-based animations, as described in detail in our How to Use UIView Animation tutorial. Replace the current todoItemDeleted: implementation with the following:

-(void) toDoItemDeleted:(SHCToDoItem*)todoItem {
    float delay = 0.0;
    
    // remove the model object
    [_toDoItems removeObject:todoItem];
    
    // find the visible cells
    NSArray* visibleCells = [self.tableView visibleCells];
    
    UIView* lastView = [visibleCells lastObject];
    bool startAnimating = false;
    
    // iterate over all of the cells
    for(SHCTableViewCell* cell in visibleCells) {
        if (startAnimating) {
            [UIView animateWithDuration:0.3 
                                  delay:delay 
                                options:UIViewAnimationOptionCurveEaseInOut
                             animations:^{
                                 cell.frame = CGRectOffset(cell.frame, 0.0f, -cell.frame.size.height);
                             }
                             completion:^(BOOL finished){
                                 if (cell == lastView) {
                                     [self.tableView reloadData];
                                 }
                             }];
            delay+=0.03;
        }
        
        // if you have reached the item that was deleted, start animating
        if (cell.todoItem == todoItem) {
            startAnimating = true;
            cell.hidden = YES;
        }
    }
}

The code above is pretty simple. It iterates over the visible cells until it reaches the one that was deleted. From that point on, it applies an animation to each cell. The animation block moves each cell up by the height of one row, with a delay that increases with each iteration.

The effect that is produced is shown in the screenshots below:

That’s pretty groovy, right?

Unfortunately, there is one little problem. You might have noticed in the code above that when the animation for the very last cell completes, it calls reloadData on the UITableView. Why is this?

As mentioned previously, UITableView manages the cell lifecycle and position where cells are rendered onscreen. Moving the location of the cells, as you have done here with the delete animation, is something that the UITableView was not designed to accommodate.

If you remove the call to reloadData, delete an item, then scroll the list, you will find that the UI becomes quite unstable, with cells appearing and disappearing unexpectedly.

By sending the reloadData message to the UITableView, this issue is resolved. reloadData forces the UITableView to “dispose” of all of the cells and re-query the datasource. As a result, the cells are all located where the UITableView expects them to be.

Unfortunately, it’s not that simple. The issue is almost resolved, but not completely. If you mark a few items as complete, then delete an item above the complete ones, you will probably notice an odd flickering effect as the green highlight rapidly shifts between cells.

Where is this strange effect coming from? It is hard to say exactly, but my thought is that the cells that are recycled are being briefly rendered before their state is updated to reflect their new status, as dictated by the cellForRowAtIndex method that you implemented in Part 1 of this tutorial.

I have tried various other hacks, and even hacks upon hacks, but unfortunately there doesn’t seem to be any way to remove this UI glitch. The bottom line is that the UITableView doesn’t like you messing with the location of its cells.

So, what next? Give up? No way!

Image Credit: quickmeme.com

Creating a Custom Table View

There’s only one thing to do: throw out UITableView and start from scratch. :] Sounds extreme? You’ll be pleasantly surprised – it really isn’t that hard and is a great learning experience.

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

Your table implementation will have to render cells within a scrolling container, so switch to SHCTableView.m and replace the existing code with the following:

#import "SHCTableView.h"

@implementation SHCTableView {
    // the scroll view that hosts the cells
    UIScrollView* _scrollView;    
}

-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        _scrollView = [[UIScrollView alloc] initWithFrame:CGRectNull];
        [self addSubview:_scrollView];
        _scrollView.backgroundColor = [UIColor clearColor];
        self.backgroundColor = [UIColor clearColor];
        
    }
    return self;
}

-(void)layoutSubviews {
    _scrollView.frame = self.frame;
    [self refreshView];
}

@end

The above code simply adds a UIScrollView to the view and sets it up on initialization. The code still needs the refreshView method referenced in layoutSubviews to be implemented, but you’ll get to that soon.

You want this table to operate in a similar manner to UITableView, but better – that is, you want a bit more control over the cell lifecycle. So add a new protocol to the project using the iOS\Cocoa Touch\Objective-C protocol template. Name it SHCTableViewDataSource. This protocol mimics UITableViewDataSource, so replace the existing code in SHCTableViewDataSource.h with the following:

// The SHCTableViewDataSource is adopted by a class that is a source of data
// for a SHCTableView
@protocol SHCTableViewDataSource <NSObject>

// Indicates the number of rows in the table
-(NSInteger)numberOfRows;

// Obtains the cell for the given row
-(UIView *)cellForRow:(NSInteger)row;

@end

As you will notice from the interface above, you have simplified things a little by removing the sections concept – your better, improved table will only have rows. There are no pesky sections to worry about. :]

The table needs to expose a property so that the datasource can be set. In order to reference the new protocol, first add an import for it to SHCTableView.h:

#import "SHCTableViewDataSource.h"

Now add the property to SHCTableView.h:

// the object that acts as the data source for this table
@property (nonatomic, assign) id<SHCTableViewDataSource> dataSource;

You’ll go for a very simple table implementation at first, when the datasource property is set, the scrollview height is set and cells created.

Add the following code to SHCTableView.m:

const float SHC_ROW_HEIGHT = 50.0f;

-(void)refreshView {
    // set the scrollview height
    _scrollView.contentSize = CGSizeMake(_scrollView.bounds.size.width,
                                         [_dataSource numberOfRows] * SHC_ROW_HEIGHT);
    
    // add the cells
    for (int row=0; row < [_dataSource numberOfRows]; row++) {
        // obtain a cell
        UIView* cell = [_dataSource cellForRow:row];
        // set its location
        float topEdgeForRow = row * SHC_ROW_HEIGHT;
        CGRect frame = CGRectMake(0, topEdgeForRow,
                                  _scrollView.frame.size.width, SHC_ROW_HEIGHT);
        cell.frame = frame;
        // add to the view
        [_scrollView addSubview:cell];
    }
}

#pragma mark - property setters
-(void)setDataSource:(id<SHCTableViewDataSource>)dataSource {
    _dataSource = dataSource;
    [self refreshView];
}

Again, the above code simplifies things: for example, the cell height is hard-coded at 50 pixels. However, the primary division of responsibilities remains the same as in UITableView – the datasource indicates the number of rows and supplies a factory method for supplying each row, whereas the table takes care of cell lifecycle and the positioning of each cell within a scrolling container.

That’s all the code you need in order to render a very basic table, so now it’s time to put it into action by modifying the view controller to use it!

The first step is to replace the existing UITableView in the nib file with your new implementation. Switch to SHCViewController.xib and remove the old table view instance from the view.

Next add a a custom control to the view controller by:

  1. Dragging a view control onto the main view.
  2. Setting the custom class for the new view to SHCTabelView using the Identity Inspector, as illustrated below:
  3. Finally, with the Assistant Editor open, Control-drag from the new view to the header file to create an outlet. Name the outlet tableView, as before. Also, don’t forget to remove the old UITableView outlet property from the header file.

In order to make the view controller work with the new table interface, there are a few other changes that need to be made. First, add an import to the top of SHCViewController.h:

#import "SHCTableView.h"

Next, you should remove the UITableView datasource and delegate protocols from SHCViewController.h, replacing them with the newly-defined datasource so that the @interface line looks like this:

@interface SHCViewController : UIViewController <SHCTableViewCellDelegate, SHCTableViewDataSource>

Next, update viewDidLoad in SHCViewController.m to configure the new table view:

-(void)viewDidLoad {
    [super viewDidLoad];
    self.tableView.dataSource = self;
    self.tableView.backgroundColor = [UIColor blackColor];
}

For the time being, the toDoItemDeleted: method you added at the beginning of this tutorial will not work, so comment it out – you’ll fix that later.

Finally, the UITableViewDataSource methods should be replaced with the following similar implementation of the SHCTableViewDataSource protocol:

#pragma mark - SHCTableViewDataSource methods
-(NSInteger)numberOfRows {
    return _toDoItems.count;
}

-(UITableViewCell *)cellForRow:(NSInteger)row {
    NSString *ident = @"cell";   
    SHCTableViewCell *cell = [[SHCTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:ident];
    SHCToDoItem *item = _toDoItems[row];
    cell.todoItem = item;        
    cell.delegate = self;
    cell.backgroundColor = [self colorForIndex:row];
    return cell;
}

Build and run your code – and there you have it:

Wow – time to celebrate! Well… almost.

The current table implementation is, unfortunately, a little too simplistic.

One of the primary functions that UITableView performs is the pooling/recycling of cell instances. On mobile devices, memory is scarce, and the most memory-hungry part of an app tends to be the user interface. Creating a table view implementation that makes all of the off-screen cells is not an efficient solution.

It’s time to fix that!

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.