Updates: On March 31st this post was updated based on feedback on twitter about using NSCache
instead of static
.
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 UIImage
and 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 UIImage
that’s 32x32
or 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 NSCache
.
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 NSCache
over static
.
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 UIImageView
and UILabel
. The UILabel
is just a standard label, nothing fancy.
In 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 [self debutsHighlightedImage]
.
1 2 3 4 5 6 7 8 |
|
In the 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 popupImage
:
1 2 3 4 5 |
|
This code uses the BBlock
method mentioned above to draw into an image that’s 120x60
or 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.
Update: NSCache
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.
Conclusion
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 NSCache
instead.
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.