Updates: On March 31st this post was updated based on feedback on twitter about using
NSCache instead of
I’ve been doing more and more drawing in code in my Cocoa projects recently and I’d like to share some tips, specifically how to cache drawing code into images for faster redraw. I’ve created a simple project for this post, the source of which can be found on GitHub.
This project is very straight forward, there is a button that when pressed shows a popup that appears for a bit then dismisses it’s self. There isn’t a single image in the project it’s all drawn in code!
The drawing code in this project was painted with PaintCode, a great new mac app for generating OS X and iOS drawing code.
Drawing in code has several advantages, your app will take up less disk space because it doesn’t have to ship with as many images. Depending on the drawing it can be easier to change a couple color values then it is to re-export a bunch of images. It also makes it easier to support multi-resolution displays because the drawing code will scale up and you don’t need to manage 1x and 2x images.
However drawing in code can cause performance issues because every time
setNeedsDisplay is called on a view the drawing code needs to re-evaluate. This is especially noticeable in
UITableView scroll performance. There are also cases where images are expected, like buttons and image views. Fortunately drawing code can be cached into images and used just like an image loaded off of disk (thanks to @badeen for originally turning me onto this approach). I’ve put together a category for
NSImage in my BBlock project that makes creating images with drawing code easier.
1 2 3
This code will cache your drawing code into a
64x64 on a retina display. The uprez for retina is automatically handled in this function, you can see what this function is doing here.
Update: There is now a new function in UIImage+BBlock.h that caches the image in a
1 2 3
Originally I recommended using
static to store the resulting image but it was pointed out that
NSCache is better for memory management. This post has been updated with more information about the advantages of
The example app
Ok let’s jump into the code of the example app! As I said this app contains a button and a popup. The popup consists of a
UIView that contains a
UILabel is just a standard label, nothing fancy.
viewDidLoad the image displayed in the popup’s
UIImageView is set to
[self popupImage] and the normal and highlight image for the button are set to
[self debutsImage] and
1 2 3 4 5 6 7 8
viewDidLoad of the example app there is also some code to setup the popup for animation, but that’s not important to this post so it was left out of the above code snippet.
These functions, that return images, are defined in PCViewController+PaintCode.m. The drawing code was split out into it’s own category so the 500 lines of drawing code doesn’t clutter the main view controller found in PCViewController.m.
The structure of these functions is similar but with different drawing code, for example here is an abridged version of
1 2 3 4 5
This code uses the
BBlock method mentioned above to draw into an image that’s
240x120 on retina.
Update: This method originally used
static to ensure that the drawing code was only executed once. However this new
BBlock method now caches the image in an
NSCache with the identifier name given. If the identifier doesn’t exist in the cache the drawing code is executed and rendered to the returned image. If the identifier exists in the cache the cached image is returned and the drawing code is not run.
This caching lets us get a little tricky in
debutsHighlightedImage. The highlight state of this button is the same icon as the normal state, but with a pink glow. So we can draw the glow and then draw the
debutsImage ontop of it!
1 2 3 4 5 6 7 8
Again because the image returned from
debutsImage is cached the drawing code will only be evaluated once so we are free to use this function multiple times without paying for multiple evaluations of the drawing code.
As I’ve mentioned the code examples here and in the sample app originally use
static UIImage to ensure that the drawing code was only evaluated once. However @henrinormak on twitter wondered about memory warnings, luckily @crizzler had suggested using
NSCache instead of static. I had never used
NSCache before so I did some reading on it and some experimenting and it is a perfect fit for caching drawing code! It is much better than
static for memory warnings because a static’s object should not be released. Also
NSCache automatically handles releasing cached objects when a memory warning is received on iOS! So when caching images into
NSCache if the app receives a memory warning
NSCache will release objects, then the next time the drawing code is run it will re-add the image to the cache.
I had really wanted to add an internal caching mechanism, like
[UIImage imageNamed] does, to
BBlock but I wasn’t sure of the best approach.
NSCache is a perfect fit for this, so a new method was added to
BBlock which you can check out here.
OS X doesn’t receive memory warnings the way that iOS does but using
NSCache is still very convenient and if they ever add memory warnings on OS X then your app will automatically take advantage of them.
BBlock contains an identical api of it’s drawing code category for NSImage.
There are lots of other ways to use drawing code cached into images. In apps where I’ve written my own
drawRect method I’ve used a similar approach where I render the drawing code into an image and then draw that image into the view, stretching it, tiling it, or creating an image that fills the view and drawing that.
1 2 3 4 5 6
This code example only works for static views were the size doesn’t change. If the size of the view changed there would need to be some sort of cache invalidation, like storing and comparing the size of the view. Also
BBlock’s internal cache couldn’t be used, but you could use a
UIImage ivar or your own
Another way to use drawing code cached into an image is to set the contents of a view’s
CALayer, this wasn’t crucial to the sample app so I didn’t want to dive into it in this post but there is a CALayer branch in the sample app that demonstrates this on the popup view if you are curious.
Obviously it’s not possible to cache everything, like a gradient in a widget that scales horizontally and vertically, but as much as possible it’s a win to cache drawing code into images. I’ve tried to make this super easy to do and wrap up all the retina and caching logic into BBlock and I hope you find it useful!
Hit me up on twitter(@_kgn) if you want to discuss this post.