More Glassy Scrolling with UITableView

by Ed on October 8, 2008

A few days ago I wrote about how to help make your table views scroll more smoothly. The tips and tricks in there definitely work, but I’ve just added another layer of smoothness on top of that. It’s rather heavy-handed, but I thought I’d throw it out there anyway.

It starts much the same, use a single view for your cell and do all your own drawing. The ultimate solution presented here though is to cache the whole damned view contents. Revolutionary? No. Extreme? Yes. But it’s the smoothest I’ve been able to get it for my situation. I’m not yet 100% sure I’ll ship with this thing turned on, but I think it’s a valid approach for you to explore for your own project.

In my case I’m slowed down a bit by database fetches, text layout, and rounded images with shadows. By only following the techniques in my last blog post on this topic it’s actually not too bad. But with this addition it’s a lot better.

It should be noted this should really only be used as a last resort due to the increase in memory required.

In order to use this method, you need to be able to uniquely identify a cell by some sort of ID (in my case it’s an NSNumber).

- (void)drawRect:(CGRect)rect
{
    if ( !_item )
        return;

    UIImage* cached = [sImageCache objectForKey:_item.identifier];
    if ( cached == nil )
    {
        CGRect    bounds = self.bounds;

        UIGraphicsBeginImageContext(
             CGSizeMake( bounds.size.width, bounds.size.height ) );

        // DRAW CONTENT HERE

        cached = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();

        if ( sImageCache == nil )
             sImageCache = [[NSMutableDictionary alloc]
            initWithCapacity:1];

        [sImageCache setObject:cached forKey:_item.identifier];
    }

    [cached drawAtPoint:CGPointMake(0,0)];
}

This is actually more expensive to draw in two ways:

  1. It means you are drawing and then blitting, instead of just drawing.
  2. The table view caches the contents of cells itself, so technically we’re buffering yet again.

But the advantage is that we have the caches around for more than just the visible cells (which is all UITableView cares about). So when things scroll back into view, they’re absolutely smooth as silk.

This does mean that as views first load in they will be less smooth than the next time they roll into view. If the user typically only scrolls in one direction, you might not see a lot of benefit. So you could decide instead to pre-cache items ahead of where the user is. So perhaps you precache the first 20 items and after that let it cache as they are encountered.

When To Dump the Cache

Obviously, memory-wise this is not cheap. How much should you save? When should you flush it?

There are many ways to deal with this:

  1. Let the system tell you when to flush. If you get a didReceiveMemoryWarning call, just flush the image cache completely.
  2. Cap the cache at some number of items and flush it when it gets to that size.
  3. Cap the cache at some number of items and drop the oldest item when a new item needs to be added (Least Recently Used).

I’m actually trying out #1 believe it or not. I also flush the cache when the view has its viewWillDisappear method called. Why keep it around if you’re not actively flushing.

Another trick is to use a timed cache. Wait for the table view to stop scrolling (possibly by listening for the scrollViewDidEndScrollingAnimation delegate method to be called. When called, start a 10 second timer. If the view starts scrolling again, clear the timer. This way, after 10 seconds of real non-interaction, you’ll get the chance to flush the cache. No point holding them around.

A timed cache was what we used in Mac OS X for menus. One of the old stand-bys for measuring ‘performance’ was how fast the menus dropped down as you dragged back and forth in the menu bar. So we used the same basic method: on first appearance, cache the image, and from then on use the cache. And then we’d set a 20 second timer to flush the buffers so we didn’t waste memory. Since then I’ve used this technique a number of times.

Here’s a quick boilerplate for how you could use a timer:

- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
{
    // the user is scrolling/dragging, we WANT the cache, don't clear it!
    [self.timer invalidate];
    self.timer = nil;
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView
                  willDecelerate:(BOOL)decelerate;
{
     // if decelerating, wait until it stops first, else start the timer now.
    if ( !decelerate )
    {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:10.0
                         target:self
                         selector:@selector(flushCaches:)
                         userInfo:nil
                         repeats:NO];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView*)scrollView
{
    // We know for sure we're done, start the timer.
    self.timer = [NSTimer scheduledTimerWithTimeInterval:10.0
                       target:self
                       selector:@selector(flushCaches:)
                       userInfo:nil repeats:NO];
}

- (void)flushCaches:(NSTimer*)timer
{
    // FINALLY. Flush it.
    [MyFancyCellView flushCaches];
    self.timer = nil;
}

Anyway, there you have it. Probably the most extreme method to get ultra-smooth scrolling while still using UITableView. Use it wisely.

{ 9 comments… read them below or add one }

Raj December 24, 2008 at 5:49 am

nice trick for light weight applications, however have you tried it with heavy applications (say 1000+ rows and lots of text columns and multiple tables)?

my only fear is the app might end up with multiple calls to didReceiveMemoryWarning this loading and freeing data multiple times

i am also juggling between more memory versus faster scroll performance. for the time being, i am going the apple way (loading data from db only when needed, displaying it, releasing it – no caching) as my app is dealing with a lot of data

i am also exploring the option of not closing the db connection (i know its risky) but having it open makes the db reads a lot faster i guess

it would be cool if you could give me some insight as to how many rows and columns are you caching in memory

cheers,
raj

http://www.iphonekicks.com

Reply

Ed December 24, 2008 at 10:10 am

Yeah, this will definitely fail miserably if you have a lot of rows. As mentioned, it’s a rather extreme approach. Definitely better suited for data sets where you know the size will be relatively small (1-200 items).

In the end, I didn’t end up using this solution myself (mostly because I had some sharing issues), but I couldn’t see a lot of user-noticeable performance improvement in the end after playing with it turned on and off.

But I do know that there are other products that use an ever more extreme case of this: they buffer the entire view into a giant scrollable bitmap. The performance is super-smooth, but clearly they can’t deal with variable length tables without making the phone explode.

Reply

Andrew Ebling January 8, 2009 at 3:41 am

Try using a layer rather than a bitmap images. Layers get cached in the shared video memory, whereas bitmaps do not.

Reply

Ed January 8, 2009 at 8:08 pm

Is there more or less room in said shared memory?

Reply

mike January 12, 2009 at 1:21 pm

I hope you can create a post on how to make a UIScrollView respond to touches and swipes at the same time and still scroll like glass… I am trying to figure out how to implemente touchesbegan, touchesmoved, touchesended and scrollViewDidScroll on the same controller. I have subclassed UIScrollView but it stopped scrolling like glass… very frustrating…

Reply

Alfredo February 21, 2009 at 10:33 am

sorry what’s the _item.identifier? O.o?

Reply

godspeedlzu May 4, 2011 at 1:20 am

I need to create tableView with a refresh View at the botton ,when pulled to the last cell, if you want to get more information shown in cells,go on to pull up,then more information can be parsed and finally shown on the cells.The effect can be seen on the Weico( a client blog app for iPhone or Ipad),but I don’t know how to realize such visual effect?

Reply

godspeedlzu May 23, 2011 at 7:00 pm

I
need to create tableView with a refresh View at the botton ,when
pulled to the last cell, if you want to get more information shown in
cells,go on to pull up,then more information can be parsed and finally
shown on the cells.The effect can be seen on the Weico( a client blog
application for iPhone or Ipad),but I don’t know how to realize such
visual effect?

Reply

Peter September 1, 2011 at 9:06 am

Use NSCache and you’re done.

Reply

Leave a Comment

Previous post:

Next post: