UIViews Cache for Reuse

I'm really excited to publish this post because it is about a simple piece of code that noticeably improves the quality of a table view and is quite generic to be used in a broad set of applications. At least it made a difference in the latest app that I'm working on...

We all know that table cells should be reused when number of rows in a table exceeds the number of visible rows. And not only because cells that do not currently fit inside table bounds consume memory but because the time required to create and initialize the cell view is long enough to be noticeable by users. In other words if you do not reuse table cells and flick through the table content then each time before a new row appears you feel a delay.

Now if you agree that cells reuse is good then take a look at UITableView's documentation. Along with cells it is possible to provide section header views. And it may happen that you actually have many sections with handful of cells in them. If you see several such headers on the screen and flick through the table you may feel the delays caused by instantiation of these views. UITableView does not allow to reuse section headers because they are generic UIViews so it seems like you are out of luck with providing a snappy table.

So there was I thinking what to do. How would you approach reuse of UIViews?

  1. Obviously you have to detect when a view could be reused and save it in a queue.
  2. Some form of cache is required to keep reusable views. There could possibly be several view types so you have to have queues for each type limited to a several views in each.
  3. And of course client code should ask for reusable view before creating a new one.

The first one is tricky and actually you can't tell for sure when view can be reused. So I assume that when view is removed from the views hierarchy then I can enqueue it for reuse. And if the view is added later to the views hierarchy I will remove it from the queue. What we need is the method that is invoked on UIView instance when it is added to or removed from the views hierarchy. Effectively this means that view is added to or removed from the window, so I override the following method:

- (void)didMoveToWindow {
  self.window ?
  [[ViewsCache sharedCache] removeReusableView:self] :
  [[ViewsCache sharedCache] enqueueReusableView:self];
}

And this is it. The rest is provided by ViewsCache class. Its API is rather simple and it is basically a wrapper around a dictionary that contains queues of reusable views. I also define a protocol for UIView that adds a reuseIdentifier property that is a type of reusable view. So here is the definition in all its glory:

@protocol ReusableView <NSObject>

@property(copy) NSString *reuseIdentifier;

@end

@interface ViewsCache : NSObject {
  NSMutableDictionary *allViews; // reuseIdentifier -> NSMutableArray:UIView
  NSUInteger capacityPerType;
}

@property(readonly) NSUInteger capacityPerType;

+ (ViewsCache *)sharedCache;

- (UIView<ReusableView> *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier;
- (void)enqueueReusableView:(UIView<ReusableView> *)view;
- (void)removeReusableView:(UIView<ReusableView> *)view;
- (void)clear;

@end

As you see I deliberately chosen the same method signature to dequeue a reusable view from the cache as we use to reuse cells. And here is how the client code looks like:

- (UIView *)tableView:(UITableView *)tableView
  viewForHeaderInSection:(NSInteger)section
{
  NSString *ViewIdentifier = @"HeaderView";
  HeaderView *view = (HeaderView *)[[ViewsCache sharedCache]
    dequeueReusableViewWithIdentifier:ViewIdentifier];
  if (!view) {
    view = [[[HeaderView alloc] initWithFrame:CGRectZero] autorelease];
    view.reuseIdentifier = ViewIdentifier;
  }
  view.textLabel.text = [NSString stringWithFormat:@"Header %d", section + 1];
  return view;
}

Strangely enough I have not noticed any significant increase in FPS when I launched a sample project in Instruments, although I clearly see the effect of this caching technique. Anyway, here is the code: ViewsCache so you can play with it and use as you see fit.

  • Digg
  • del.icio.us
  • Reddit
  • StumbleUpon
  • LinkedIn
  • E-mail this story to a friend!
  • Print this article!

  • jeremyk44

    This worked great! I was storing all the section headers I was using and then handing them back but that didn't work in a couple cases where I was scrolling and the view actually disappeared.

    Thanks!

  • http://www.dimzzy.com/ dimzzy

    Good to hear this! What do you mean "view actually disappeared"? Headers are cached so obviously you should create new ones when cache is empty and there is nothing to reuse; or you meant something else?

  • jeremyk44

    I meant something else. I was storing an array of all my header views as it asks for headers a lot on scrolling and I didn't want to be making the same header over and over. It was working fine generally but I had it autoscrolling to a specific row and sometimes one of the headers wouldn't draw. Scrolling it out of the view and back would get it to show up. I think it had something to do with the framework wanting two copies of the same header for some reason. Your resuse code works fine because it can give back two of the same header if that is what is really needed.

  • http://www.dimzzy.com/ dimzzy

    Maybe you could check retain count of the header view - if table additionally retains the header you maybe could detect it this way and don't reuse the header view.

  • Bentford

    This worked for me. Thanks.

  • Jim

    Thanks so much! This saved me a ton of time. Was not aware of the trick with [UIView didMoveToWindow] method..

  • http://www.facebook.com/aaron.sarazan Aaron Sarazan

    Hey this is great! Small request-- there's not an explicit license in the source. Any chance of getting a BSD license thrown on there so it can be used commercially? Thanks :-)

  • http://www.dimzzy.com/ dimzzy

    Thanks! Actually I don't care what you will do with it but if you want some license let's add BSD to it )))

  • Mark Krenek

    Look out that the view you get out of the cache doesn't have it's alpha set to 0. I've seen instances where UITableView sets the alpha to 0. And if you turn around and hand one of those back to the table view, it doesn't set the alpha back to 1.

blog comments powered by Disqus