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
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Recycling Cells

First, you need a structure for your table to store cells for reuse, so add the following instance variable to SHCTableView.m (below the existing one for _scrollView):

    // a set of cells that are reuseable
    NSMutableSet* _reuseCells;

Next, initialize the NSMutableSet you added above at the end of initWithCoder::

        _reuseCells = [[NSMutableSet alloc] init];

Now, you need to make refreshView a little more intelligent. For instance, cells that have scrolled off-screen need to be placed into the reuse pool, _reuseCells. Also, when gaps appear, you need to fill them with new cells, either from the pool or obtained via the datasource.

Add this logic by replacing the current implementation of refreshView with the following:

// based on the current scroll location, recycles off-screen cells and
// creates new ones to fill the empty space.
-(void) refreshView {
    if (CGRectIsNull(_scrollView.frame)) {
        return;
    }
    // set the scrollview height
    _scrollView.contentSize = CGSizeMake(_scrollView.bounds.size.width,
                                         [_dataSource numberOfRows] * SHC_ROW_HEIGHT);
    // remove cells that are no longer visible
    for (UIView* cell in [self cellSubviews]) {
        // is the cell off the top of the scrollview?
        if (cell.frame.origin.y + cell.frame.size.height < _scrollView.contentOffset.y) {
            [self recycleCell:cell];
        }
        // is the cell off the bottom of the scrollview?
        if (cell.frame.origin.y > _scrollView.contentOffset.y + _scrollView.frame.size.height) {
            [self recycleCell:cell];
        }
    }
    
    // ensure you have a cell for each row
    int firstVisibleIndex = MAX(0, floor(_scrollView.contentOffset.y / SHC_ROW_HEIGHT));
    int lastVisibleIndex = MIN([_dataSource numberOfRows],
                               firstVisibleIndex + 1 + ceil(_scrollView.frame.size.height / SHC_ROW_HEIGHT));
    for (int row = firstVisibleIndex; row < lastVisibleIndex; row++) {
        UIView* cell = [self cellForRow:row];
        if (!cell) {
            // create a new cell and add to the scrollview
            UIView* cell = [_dataSource cellForRow:row];
            float topEdgeForRow = row * SHC_ROW_HEIGHT;
            cell.frame = CGRectMake(0, topEdgeForRow, _scrollView.frame.size.width, SHC_ROW_HEIGHT);
            [_scrollView insertSubview:cell atIndex:0];
        }
    }
}

// recycles a cell by adding it the set of reuse cells and removing it from the view
-(void) recycleCell:(UIView*)cell {
    [_reuseCells addObject:cell];
    [cell removeFromSuperview];
}

// returns the cell for the given row, or nil if it doesn't exist
-(UIView*) cellForRow:(NSInteger)row {
    float topEdgeForRow = row * SHC_ROW_HEIGHT;
    for (UIView* cell in [self cellSubviews]) {
        if (cell.frame.origin.y == topEdgeForRow) {
            return cell;
        }
    }
    return nil;
}

The two phases, remove off-screen cells, and add new cells to fill gaps, should be apparent in refreshView. The method also makes use of an interesting support method, cellSubViews, which returns an array of cells from the scroll view.

Implement that method by adding the following code:

// the scrollView subviews that are cells
-(NSArray*)cellSubviews {
    NSMutableArray* cells = [[NSMutableArray alloc] init];
    for (UIView* subView in _scrollView.subviews) {
        if ([subView isKindOfClass:[SHCTableViewCell class]]) {
            [cells addObject:subView];
        }
    }
    return cells;
}

The subViews property of a UIScrollView not only exposes the views that you have added to it yourself, but it also returns the two UIImageView instances that render the scroll indicators.

This can be a bit confusing! (See the StackOverflow question on UIScrollView Phantom Subviews). That's why you check for the subview class before adding it to the list of cells to be returned – so as to make sure that you only return the SHCTableViewCell instances.

Since the above method references SHCTableViewCell, add the following import to the top of the file:

#import "SHCTableViewCell.h"

The code above does a great job of recycling cells, but it wouldn't be of any use without providing a mechanism to allow the datasource to pull items out of the pool!

It’s time to make a few more changes to the table view interface in SHCTableView.h. Add the following method prototypes:

// dequeues a cell that can be reused
-(UIView*)dequeueReusableCell;

// registers a class for use as new cells
-(void)registerClassForCells:(Class)cellClass;

Rather than adopt the old (pre-iOS 6) UITableView approach of recycling cells – where you have to ask for a cell, check whether it is nil, then create a new instance of cell – the above sets it up as it works post-iOS 6. You simply register a Class which defines the cell type. The table can then perform the nil check internally, and the method always returns a cell, either from the reuse pool or by creating a new one.

Before implementing the above methods, you need to add a supporting instance variable to SHCTableView.m (right below _reuseCells):

    // the Class which indicates the cell type
    Class _cellClass;

Now add the method implementations:

-(void)registerClassForCells:(Class)cellClass {
    _cellClass = cellClass;
}

-(UIView*)dequeueReusableCell {
    // first obtain a cell from the reuse pool
    UIView* cell = [_reuseCells anyObject];
    if (cell) {
        NSLog(@"Returning a cell from the pool");
        [_reuseCells removeObject:cell];
    }
    // otherwise create a new cell
    if (!cell) {
        NSLog(@"Creating a new cell");
        cell = [[_cellClass alloc] init];
    }
    return cell;
}

And now the final steps – making use of the cell reuse functionality!

Switch to SHCViewController.m and add the following to the end of viewDidLoad to specify the cell type:

    [self.tableView registerClassForCells:[SHCTableViewCell class]];

And update cellForRow: by replacing this line:

    SHCTableViewCell *cell = [[SHCTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:ident];

With the following (you can also remove the unnecessary ident variable, since it's no longer needed):

    SHCTableViewCell* cell = (SHCTableViewCell*)[self.tableView dequeueReusableCell];

If you build and run the code, you will find that your app looks exactly the same as before:

Or does it? Try scrolling. Notice that you don't see all of your to-do items! Most of the time, you'll probably see one or two items beyond the edge of the screen, but that's it. What is going on?

This is because the new code displays only the cells that are in view. When you scroll up or down, you need to know about the scrolling, so that the view can refresh the display. This part is fairly easy to implement. :]

First, set SHCTableView up as a UIScrollViewDelegate in SHCTableView.h:

@interface SHCTableView: UIView <UIScrollViewDelegate>

Next, you have to set up your scroll view to use your SHCTableView as its delegate. Add the following code to initWithCoder: in SHCTableView.h, right after you create the _scrollView instance:

		_scrollView.delegate = self;

Finally, you have to respond to scrollview events, specifically when the user scrolls the scrollview. Add this code to do that:

#pragma mark - UIScrollViewDelegate handlers
-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
    [self refreshView];        
}

Now build and run your app, and you should be able to scroll all the way to the end of your list:

IOS Simulator  iPhone  Retina 3 5 inch  iOS 6 0  10A403

But it still looks the same as before, doesn't it? I know what you’re thinking: "If it looks just the same, where’s my recycling?"

Scroll the to-do list up and down while keeping an eye on your console output. You should see the NSLog messages that indicate the recycling is working properly:

When the table is first displayed, the pool is empty, hence a number of cells are created (with a couple of extra ones for you lucky iPhone 5 users!). Subsequent scroll interactions cause cells to be placed in the pool as they move off screen, and then pulled out again as space appears.

The net result is that no matter how long the list is (perhaps you are a busy person with 10,000 to-dos in your list), the application only ever allocates a small handful of cells.

Colin Eberhardt

Contributors

Colin Eberhardt

Author

Over 300 content creators. Join our team.