Apple introduced IBDesignable objects and IBInspectable properties to a lot of fanfare at WWDC 2014. In the demo, it was hailed as a major feature that would allow developers to create entire UI frameworks to brand their apps and give them a custom look and feel while still allowing them to design their UIs visually in Interface Builder. If you’re unfamiliar, here is a great NSHipster article about it.
Unfortunately, like a lot of Apple’s Xcode features, it was plagued by problems. Interface Builder has had a lot of trouble in the past two years rendering and linking such UI frameworks, leading to slowness and crashes. It still lacks obvious features like the ability to give custom names to IBinspectable properties and support for enums or other data types.
The good news is that it finally seems like it has stabilized a lot with Xcode 8 and Swift 3. So I wanted to give a demo of how to use the feature to create a common iOS interface element: a .
Anatomy
What is a pill label? We see “pill labels” everywhere in iOS – usually in table view cells, maybe calling out a number of unread items, or maybe a tag on an object. It’s a one-line label, with a colored background, and perfectly rounded ends. It’s like a rounded rect, except the radius of the rounded corner is such that there is not a vertical line.
Getting Started
As ubiquitous as pill labels are, creating them is surprisingly difficult. You might think, “just add a background color and corner radius,” right? Unfortunately, there is more to it than that.
import UIKit @IBDesignable class PillUILabel: UILabel { func setup() { layer.cornerRadius = frame.height / 2 clipsToBounds = true } override func awakeFromNib() { super.awakeFromNib() setup() } override func layoutSubviews() { super.layoutSubviews() setup() } override func prepareForInterfaceBuilder() { super.prepareForInterfaceBuilder() setup() } }
This sets up an IBDesignable UILabel called PillUILabel. The setup() function applies a corner radius to the layer, and makes it half the height of the label itself to make the ends look round. Then we call the setup() function at various times in the view lifecycle – when the view awakes from a XIB file, when it lays out subviews (typically after a rotation or resizing event), and when it should be prepared for Interface Builder.
So if we add a label to a storyboard, set it’s custom class to PillUILabel, set a background color, and and set a text color, we get this:
Since the background is within the view, it appears cut off. Clearly we need to make it wider so the rounded corners don’t touch the text. We could use autolayout to set a width, of course… but that means that we couldn’t set arbitrary text inside the label because it could be longer. We want it to be the normal, expected width of the label, plus a little padding on the ends. How can we accomplish that?
Intrinsic Content Size
Elements like UILabel have what’s called an intrinsic content size – in Swift 3, it’s a computed property named intrinsicContentSize. This is what we need to adjust. We have the ability to override this property and return our own value for it, and Interface Builder will use this value.
override var intrinsicContentSize: CGSize { let superSize = super.intrinsicContentSize let newWidth = superSize.width + superSize.height let newHeight = superSize.height let newSize = CGSize(width: newWidth, height: newHeight) return newSize }
I’ve chosen to use the view’s height as the amount to increase the width by. Adding this to the PillUILabel will cause Interface Builder to adjust accordingly:
This does what we set out to do, but there is still an obvious problem – the alignment. The text is not aligned in the middle of the label, it’s aligned left (the default alignment of UILabels). My preference is to just set the text alignment to always be centered in the setup() function of SympUILabel.
func setup() { layer.cornerRadius = frame.height / 2 clipsToBounds = true textAlignment = .center }
If you don’t want to do this, you could just set the text alignment in Interface Builder. But you’d have to do this every time, and if you are using intrinsic content size then you would never really want any option other than center alignment. So, once center alignment is set, we have this:
At this point, we have a pill label that can be reused in many applications. It will be rounded no matter the text size.
Going Further
Personally, I’d like the ability to adjust the pill label slightly to set a custom amount of padding on the sides and on the top. We can accomplish that by adding IBInspectable properties.
As the name implies, IBInstpectable properties are properties on IBDesignable objects that can be altered in the Inspector pane of Interface Builder. Only a limited number of variable types are supported, which you can read about here. In this case, we want to add two IBInspectable properties for vertical and horizontal padding that are GCFloats.
@IBInspectable var verticalPad: CGFloat = 0 @IBInspectable var horizontalPad: CGFloat = 0
Normally, I would advocate a more verbose name like verticalPadding and horizontalPadding, but if we do that, we get this in Interface Builder:
Interface Builder attempts to derive the human-readable name of the property from the actual variable name, assuming camel cased variable names. And it severely limits the space they make available in the Inspector name for your variable names. These are two examples of where I think Apple has really dropped the ball on providing useful tools to us developers. The label for the property should be way bigger, and there should be some way in code to specify an alternate display name for the property, perhaps through headerdoc comments. But I digress. Back to the example.
Once we have created the IBInspectable properties, we need to implement them in our override of the intrinsicContentSize property. This is because those variables should alter the intrinsic content size of our label.
override var intrinsicContentSize: CGSize { let superSize = super.intrinsicContentSize let newWidth = superSize.width + superSize.height + (2 * horizontalPad) let newHeight = superSize.height + (2 * verticalPad) let newSize = CGSize(width: newWidth, height: newHeight) return newSize }
Once we update this, we can head back to our storyboard, let the code re-build, and start interacting with our view!
Problems
As of Xcode 8.1, there seems to be an intermittent bug with embedding these pill labels in stack views and then adjusting the intrinsic content size. It sometimes makes the pill view take over the entire stack view in Interface Builder (although it works fine at run time). I can’t seem to duplicate it consistently, but please post in the comments if you see this, or any other problems.
Source
You can download or clone code for this project at github.