Helper class that allows you to draw views (a)synchronously to a UIImage with caching for great performance.
Objective-C Ruby
Latest commit ef64df9 Feb 12, 2014 @NachoSoto NachoSoto Merge pull request #12 from NSElvis/patch-2
Update README to bump version in Podfile.

README.md

MSCachedAsyncViewDrawing

Helper class that allows you to draw views (a)synchronously to a UIImage with caching for great performance.

Description

(This assumes you know a bit about CoreGraphics and how some things like blending work. If not, go read this post in the Twitter Engineering Blog).

So you have a UITableView in your application that scrolls slow. You decide to implement the cell drawing entirely in CoreGraphics implementing -[UIView drawRect:] in your cell. This is perfect, until you have to draw images. CGContextDrawImage is really slow compared to using UIImageView.

For this reason, many times you'll find yourself preferring to use UIImageView even if some compositing has to happen on the cell, because rendering images with it is FAST due to the crazy optimizations that it implements internally.

But sometimes you do have to use CGContextDrawImage, because you have to do something more complex like masking, clipping, etc. Wouldn't it be great if you could still do that, but pass the result to a UIImageView easily, so that you get the benefit from both worlds? That's what MSCachedAsyncViewDrawing does.

When to use MSCachedAsyncViewDrawing

Needless to say you shouldn't just go ahead and apply this to all of the UIViews in your app. There's a drawback in this approach, as you're incurring in higher memory usage by storing the result of the all the drawing operations. The only way to know if using MSCachedAsyncViewDrawing improves or not the performance in your particular case, is to try it out and compare. As a general rule on when it makes sense to use it would be when -drawRect: is becoming a bottleneck, specially if it's using CGContextDrawImage inside. This can happen when you have many complex views in the cells of a UITableView.

Sample Project

The sample project contains two view controllers that contain a table view in which every row has 3 views that implement -drawInRect:. One of them uses MSCachedAsyncViewDrawing and the other one doesn't. This is an example on how to use this class and its performance benefit. Install the sample app on your iOS device and compare. It's also a typical use case for this class, since there are many views on screen at the same time, and they all have to render a UIImage, this becomes a bottleneck. MSCachedAsyncViewDrawing makes this asynchronous, hence not blocking the main thread and getting perfect scrolling performance, and it also prevents the views from rendering more than once.

How to use it

  • Install using cocoapods by adding pod 'MSCachedAsyncViewDrawing', '~> 1.0.1' to your Podfile.
  • Or add the MSCachedAsyncViewDrawing.(h+m) files to your project.

This is the main method in MSCachedAsyncViewDrawing:

- (void)drawViewAsyncWithCacheKey:(NSString *)cacheKey
                             size:(CGSize)imageSize
                  backgroundColor:(UIColor *)backgroundColor
                        drawBlock:(MSCachedAsyncViewDrawingDrawBlock)drawBlock
                  completionBlock:(MSCachedAsyncViewDrawingCompletionBlock)completionBlock;

The block types are declared like this:

typedef void (^MSCachedAsyncViewDrawingDrawBlock)(CGRect frame);
typedef void (^MSCachedAsyncViewDrawingCompletionBlock)(UIImage *drawnImage);

MSCachedAsyncViewDrawing is going to take the drawBlock and call it on a background thread, passing it the CGRect that you can pass to a -drawRect: method of a view. When it's done, it's going to cache the UIImage object with the specified cacheKey and call your completionBlock with it.

A subsequent call with the same cacheKey will result in the immediate call of the completionBlock without calling the drawBlock because it'll grab the rendered image from the cache.

The cache is implemented using NSCache, so you don't have to worry about putting caching too many images, because iOS is going to take care of evicting obejcts as the available memory goes low.

If you prefer to block the UI while the rendering is happening, beacuse you want to make sure that the image view is not empty at any point, you can use this other method, which immediately returns the UIImage object:

- (UIImage *)drawViewSyncWithCacheKey:(NSString *)cacheKey
                                 size:(CGSize)imageSize
                      backgroundColor:(UIColor *)backgroundColor
                            drawBlock:(MSCachedAsyncViewDrawingDrawBlock)drawBlock;

Compatibility

  • MSCachedAsyncViewDrawing is compatible with iOS5.0+.
  • MSCachedAsyncViewDrawing uses ARC. To use in a non-ARC project, mark MSCachedAsyncViewDrawing.m with the linker flag -fobjc-arc.

License

MSCachedAsyncViewDrawing is available under the MIT license. See the LICENSE file for more info.