It's really worth noting that there are substantial performance ramifications to your choice here, in terms of CPU time for generation, memory usage, I/O, and GPU time for rendering.
The resizable image background technique will be the fastest option (provided you don't need to programmatically tweak the appearance). Next would be the full-sized background (which needlessly wastes memory). Then the CG-based approach, which could actually perform better than the full-sized background if you had generated a resizable image—the technique presented here needlessly wastes memory and CPU time by generating a full-width image. But it will still perform worse in basically all cases than the resizable-image-from-disk technique, so you would only want to do that if the parameters need to be tweaked at runtime.
The CAGradientLayer-based approach (as written) is a very poor idea unless you need the animated transitions because it requires extremely expensive off-screen drawing passes due to the masking. If your situation permits you to use "overdrawn" masking (as I discussed in WWDC 2012's "Polishing Your Rotation Animations"), this would actually perform quite well—less memory consumption than all the other options; small rendering cost each frame. See also WWDC 2011's "Understanding UIKit Rendering" for more on graphics performance with UIKit.
I guess it's also worth noting that the cost of masking CALayers with a borderRadius is much lower in iOS 6 than iOS 5, but don't go nuts: it's still way higher than all these other options.
Great comment, thanks for making it. There's nothing like having a domain expert explain the tradeoffs using specifics.
A while back I wrote a reusable class that draws a single button using three overlapping CAGradientLayers each .5 pixels larger than the next layer drawn on top of it. With this arrangement I can then specify top and bottom values for the "outer gradient", "inner gradient" and "body gradient" producing beautiful buttons that are 100% adjustable in code. Since 80% of my skills lie in the development realm vs. the design realm this class has been insanely useful for me across multiple projects.
But obviously not efficient. If I wanted to refactor my class to still have the three levels of run-time rendered gradients along with rounded corners, what would be the most efficient method?
Render the gradients into a resizable UIImage via CG. That differs from approach #2 in the article, which does the same thing, but generates a non-resizable UIImage.
To make a resizable image at runtime, draw into an image as described in approach #2, but make the image have width of left cap + 1 point + right cap, then use -[UIImage resizableImageWithCapInsets:] to generate a wrapper with the correct resizing behavior.
Then: make sure that if you have 100 buttons on-screen which all use the same gradients, you end up reusing the same generated UIImage. You don't want to redraw the same thing for each of them.
I wish Apple would publish a list of common misconceptions including things like this. I see lots of code where tons of UI drawing is being done that could (and should) be done with images because it's "hardcore" and "fast". But no- it's over complicated and slow!
Many of these are places where people are caching rasterizations of deep layer hierarchies to avoid paying the compositing cost on a per-frame basis. But if you do that with drawRect:, you're using the CPU (not the GPU); if you use your own images, you're on the hook for your own caching, which you're likely to mess up. Such cases should probably use -[CALayer setShouldRasterize:] instead.
For more about this stuff, check out the two WWDC sessions I mentioned above.
It really just matters what you want to achieve, and if you are just rendering one button, all techniques perform fine.
What if you want buttons that can be any color? Then using resizable images doesn't work very well.
I use custom drawing code for one button in an app because I have a color wheel that let's you color all controls in the app to whatever theme you like. And for some other buttons, I use images to have more graphical control.
It's not really true that all techniques perform fine if you're rendering just one button.
If you're using a masked CALayer, and that layer's in a scroll view or is otherwise animating around the screen, there's a very real chance that you'll drop frames, just from that layer's off-screen rendering pass. Depends on how big the button is.
Certainly, though, if you need parameterizable imagery, you need parameterizable imagery, and so you can't load them from disk. But you can still make your runtime-generated images resizable!
First, I'm sure you know better than me about the deep technical issues, given you make UIKit. I also really appreciate that you are on this forum, talking about it!
That said, I have three points:
1) I would never start thinking about making a UI element by thinking about performance. I would build to my functional requirements, and then optimize if necessary.
2) I use this class that draws a button programatically, and I use it inside a UITableView, on a screen that auto-rotates. I have never had an issue, and I developed this code for early iPhones, in 2010. Can you comment on my code in particular, which uses a CAGradientLayer? I have never noticed any issues with this code in practice.
3) I never thought about generating UIImages of various colors at runtime - that does sound like a cool technique. It would be nice if stylish butons were built into UIKit - that would have saved me fumbling early on.
Regarding the TripComputerButton, I'm a little surprised that this is performant in a table view! I'm guessing your buttons are fairly small (< 100x100)?
For this table, I also render the cells once and don't dequeue them, because I like to achieve the transparent, gradient effect for each table section, over the classic vertical lines of the grouped UITableView. https://www.dropbox.com/s/67xfol10c0kkqb2/TripComputerCellBa...
In the end, it is really great to have detailed knowledge of UIKit and CoreGraphics, so you save yourself time optimizing on the end, if you just know the right thing to do. And I think your comments about the accuracy of the blog's claims are righteous and a good addition. I envy your fundamental grasp of this stuff, and I try to keep learning, even as I fumble towards what I want.
Drawing controls in code, unless you gain something other than the space saved for bitmaps, is a waste of time and energy. You gain nothing by drawing the gradients for buttons in code.
If it all ends up in that format anyway, you might as well do what Apple does 95% of the time: just use bitmaps. In mobile, saving cycles is more important than saving storage.
Except the ability to resize your UI widgets. If your UI is almost entirely static, then sure, bundling a buttload of images is fast, easy on your designers, and your engineers won't hate you.
But if your UI is of the sort that has highly variable sizing and layouts, then you probably do want to start writing custom draw code that removes you from having to maintain a hundred variants of the same asset in your bundle - say, multi-line gradient buttons. It also allows easier abstraction (say, tinted gradient buttons) without your designer having to painstakingly generate what is essentially the same asset save one feature.
Programmatic UIs also make it easier to do related A/B tests - otherwise testing a button's color would involve shipping updates with every candidate in the bundle.
Like all things, be judicious and smart - though the trend I'm noticing is that we're moving beyond simple UIs on iOS, and there's increasing demand for the sort of UIs that demand this level of flexibility. The performance of recent iOS devices have also been such huge leaps that the penalty of not using pre-baked images everywhere is minimal in most cases.
Custom drawing also has a lot of optimization use - this is a surprising little-known trick for custom UITableViewCells. Compositing is still a very heavy load, avoid having complex view hierarchies for high-performance UI components (a UITableViewCell is high perf in most instances). Consider flattening your views into something that fits into your drawRect call - drawing text directly instead of UILabels, drawing images directly instead of UIImageViews, etc.
Also, a surprisingly little known trick is CALayer.shouldRasterize - setting this flag will rasterize everything in that layer and below and cache it. This incurs a heavier hit on redraw, but for objects that do not need to be redrawn often it's worth it - this allows you to have baked-image performance while still doing custom drawing.
Also, calling -imageNamed to load your images will automatically cache the contents. Numerous Apple engineers have told me not to draw UIs in code and just to use images instead, so although it's a fun challenge, there really is no upside. Even animations in iOS like the jiggling/opening trash are just a series of PNGs using UIImageView animation.
there are plenty of upsides to drawing UIs in code. You get resizable controls, controls you can change the color on, controls that can be added easier programatically, and smaller app bundles.
> "You also have that with images, so that does not apply."
No, it very much still does. The only sort of resizing you get with images is whatever neatly fits within a 9-patch.
Which is to say, if your center patch is non-uniform (gradient? textured surface?) you lose resizability.
So yes, if you design a very standard UI that sails very close to the default iOS look and feel, you will be using stretchable images for almost everything. Which is to say, single-line elements that do not resize vertically at all, vertical linear gradients as an aesthetic theme instead of texturing (which is becoming a thing now), etc.
The moment you depart those shores though, images start becoming a liability instead of an asset, and you'll need to do a lot more custom work to get your widgets to resize properly.
Using bitmaps also has the advantage that the bitmaps can be shared when the control is displayed more than once. It doesn't matter so much for the newest 1GB devices, but on older devices that only have 128 or 256MB it can make a difference.
A recently introduced second option consists
in using a resizable image as a button background
after having set its resizable and non-resizable
areas in code. Start by making a pill-shaped
background image in your graphic editor.
Not accurate. -[UIImage stretchableImageWithLeftCapWidth:topCapHeight] has been around since the first public SDK release. Although, I must say the -resizableImageWithCapInsets: methods added in iOS 5 and 6 are far more powerful.
Also, here are a bunch of UIButton subclasses that I think are pretty neat that demonstrate some of the techniques described in the article, plus others:
I'd like to throw in the minor nitpick that stretchable/capped images are not iOS 5+, but in fact date back to iPhoneOS 2.0, with -[UIImage stretchableImageWithLeftCapWidth:topCapHeight:]
All said and done, however, how is "here, use the APIs provided to you, or draw the graphics yourself" at all "taming" UIButton?
Nice article. I'm still excited about getting my first beta of Pixate (http://www.pixate.com/). I saw that kickstarter project here and bought in, twice (they had lowered the goal). Then we'll have a new way of customizing iOS controls. I did get the shirt, but I'd really the the beta. It should be coming any day now.
Thanks for the link. I will have to take a harder look at it as it wasn't quite obvious to me as to what it truly provides at the moment. Again, thanks and I will clone it when I get to my laptop.
Not connected to the tutorial but I was trying to look at their other posts and I seem to get redirected to their learn.thoughtbot.com where they show their books, webcasts, and workshops which isn't bad but I just want to read their other posts.
It depends what you want to do - as has been alluded to here, the image based approach will be the best option in terms of responsiveness. If you draw your entire button with core graphics you can then rasterize it, but the drawing process is in itself computationally more expensive than rendering out the image.
Apple has good docs on it[1]. I find the easiest solution is to create a custom cell nib and style that. If I need multiple custom cells I tend to do it in code. This was something I always found difficult to do well but over the years it's improved a lot (especially with the appearance API in iOS 5).
The resizable image background technique will be the fastest option (provided you don't need to programmatically tweak the appearance). Next would be the full-sized background (which needlessly wastes memory). Then the CG-based approach, which could actually perform better than the full-sized background if you had generated a resizable image—the technique presented here needlessly wastes memory and CPU time by generating a full-width image. But it will still perform worse in basically all cases than the resizable-image-from-disk technique, so you would only want to do that if the parameters need to be tweaked at runtime.
The CAGradientLayer-based approach (as written) is a very poor idea unless you need the animated transitions because it requires extremely expensive off-screen drawing passes due to the masking. If your situation permits you to use "overdrawn" masking (as I discussed in WWDC 2012's "Polishing Your Rotation Animations"), this would actually perform quite well—less memory consumption than all the other options; small rendering cost each frame. See also WWDC 2011's "Understanding UIKit Rendering" for more on graphics performance with UIKit.
I guess it's also worth noting that the cost of masking CALayers with a borderRadius is much lower in iOS 6 than iOS 5, but don't go nuts: it's still way higher than all these other options.