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?
- Obviously you have to detect when a view could be reused and save it in a queue.
- 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.
- 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.






