Designing for iOS: Taming UIButton
robots.thoughtbot.com
robots.thoughtbot.com
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.
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.
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!
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.
https://www.dropbox.com/s/5w938hx7ittzhc2/TripComputerButton...
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.
http://a1296.phobos.apple.com/us/r1000/118/Purple/v4/56/00/c...
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.
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?
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.
For more about this stuff, check out the two WWDC sessions I mentioned above.
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.
You also have that with images, so that does not apply.
You do get the rest, which has to be weighted against lower performances and higher maintainability costs.
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.
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.
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:
http://www.cocoacontrols.com/platforms/ios/controls/bbutton
http://www.cocoacontrols.com/platforms/ios/controls/psstoreb...
http://www.cocoacontrols.com/platforms/ios/controls/gloss-ca...
All said and done, however, how is "here, use the APIs provided to you, or draw the graphics yourself" at all "taming" UIButton?
Does anyone know how?
[1] http://developer.apple.com/library/ios/#documentation/UserEx...