Sometimes I long for the day when I get to create an iOS app that just uses Apple’s default interface element. Of course, this doesn’t last long, because using and abusing
UIView is fun. Over the years I’ve picked up a few tips and tricks for making the most out of your time. The best tip is, first and foremost, you need to start copying Apple.
This cannot be emphasized enough. Apple’s framework developers aren’t perfect and they have made mistakes, but overall they provide excellent frameworks, especially for UI development. Make your class interfaces look like Apple’s class interfaces. Try to subclass Apple’s own elements before going off and creating your own. Don’t reinvent the wheel.
That being said, any iOS app of a decent size will create many custom
UIView subclasses and learning how to properly subclassing
UIView will reap benefits.
- You’ll get a lot of useful behavior right out of the box.
- You increase readability of your code and make it easier for future developers to understand what you’ve written.
Here are some tips for abusing
Subclass the right class
If you are creating a generic control, subclass
UIControl. If you are creating a custom button, subclass
UIButton (or just create a custom button! That’s usually all you need). If you are creating a custom switch, subclass
UISwitch (lols, just kidding.
UISwitch is a terrible and fragile class to subclass. Write your own switch if you need something custom).
I see so many developers implementing complex touch event listeners in their
UIView subclasses or a needless delegate protocol when they could have just gotten the same behavior for free by using the proper superclass.
Your first attempt at custom layouts should always use
UIViewAutoResizingMask (this changes in iOS 6.0, read more on developer.apple.com). Setting these up takes a trivial amount of time and you stand a pretty good chance of eliminating your custom layout code. Writing flexible and extensible layout code is very hard, which is why most developers don’t bother and just hard-code a bunch of magic numbers into their view layouts. Your layout may always be 320px wide today, but someday you’re going to enable landscape orientation in your app and having to go back in and debug your layout all over again is going to be a major headache.
Realistically speaking, you’re probably going to have layouts that springs and struts can’t handle. If you need to do custom layouts, do them in the correct location, by overriding
layoutSubviews. You should always put your layout code in this method. Here’s some great reasons why you should be doing this.
layoutSubviewswill only be called once per run-loop. You can call
[self setNeedsLayout]as much as you want without worrying about taking a performance hit laying out your views needlessly.
layoutSubviewsis common knowledge. Other developers (and yourself in 6 months) should know to look here for layout code. No need to try and scan unfamiliar code to find the method doing layout; it’s always the same one.
- Putting all your layout in one method makes handling state changes much more robust. Big Nerd Ranch has a writeup you should read.
It’s fairly common to have your layout depend on the properties of your subviews. This is how people tend to handle layout and it’s wrong.
1 2 3 4 5 6 7 8 9 10 11 12
The biggest issue with doing this is that it scatters your layout code all over your class. Once you get two, three, four or more properties that influence your layout, each property setter becomes an incomprehensible mess of layout code. Don’t do this. A much better way to handle property-based layouts is to override
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
You can feel free to call
[self setNeedsLayout] as much as you want, those calls will be consolidated and the layout methods will only be called once per run loop. For complex layouts, this can be a massive performance win.
- (CGSize)sizeThatFits:(CGSize)size is the unsung hero of custom
UIView subclasses. This method can be used to set minimum sizes, maximum sizes, and optimal sizes. The trick here is that the
sizeToFit calls through to
sizeThatFits: with the current size, then resizes the view based on the response.
sizeToFit: is always the method that you should override, it’s the designated initializer of sizing methods. A great use for this method is to allow users to initialize your class with a zero sized rect while still maintaining a proper frame.
1 2 3 4 5 6 7 8 9 10 11 12 13 14
Now, if someone really wants to initialize your view to an absurd size you can prevent this by overriding
setFrame:, but in my experience, when someone resizes something to the wrong size, it’s obvious what they did wrong. When they’re attempting to resize something and it just won’t become the correct size, it’s usually a mystery why. Don’t surprise your users, let them shoot themselves in the foot, but make it obvious that they are shooting themselves in the foot.
Respond intelligently to weird frame sizes
UIView is instantiated by calling
init, the default implementation will call through to
initWithFrame: and pass
CGRectZero. This is very common, and it shouldn’t cause your view to blow up or completely break. If you have a minimum size that your control should be, resize yourself in
initWithFrame. If you don’t have a minimum size, make sure your subviews are pinned to the correct edges of your bounding rect so that they behave correctly when your view is resized. These two views should both have the same frame and be layed out identically:
1 2 3 4
UIView that blows up when instantied with a size zero frame is a perpetual source of developer annoyance. Don’t do this. If you need to use default frame sizes, provide an initializer that doesn’t take a frame as a parameter, and override
initWithFrame: to ignore the input frame size. Make a note in the documentation to this effect.
The default behavior of
pointInside:withEvent: returns YES if the point is within your bounds. Override this method if your shape does not completely fill its bounds. For example, if you have a button with a transparent center region, you may not want to respond to taps in that region. Override this method to return NO for any points within that transparent area.
Even more useful, if you have a control that is smaller than the Apple HIG recommended guidelines of 44x44px, you can respond to touches outside your bounds, effectively increasing the tap target for a given button.
Don’t override drawRect:
drawRect: causes a performance hit. I haven’t profiled it, so I don’t know if it’s a signifigant performance under normal circumstances (it’s probably not), but in most cases, folks who override
drawRect: can accomplish what they want much easier by setting properties on a view’s layer property. If you need to draw an outline around a view, here’s how you would do it:
1 2 3 4 5 6 7 8 9
Learning how to properly subclass
UIView will save you a lot of coding, a lot of debugging and will make it much easier for future maintainers of your code to read and understand what you’ve done, arguably the most important consideration for a developer.