While UIControl
has contentHorizontalAlignment
and contentVerticalAlignment
, neither of these properties is sufficient to allow us to specify a re-alignment of the icon and title in the button. So let’s add an enumeration to our Button
class.
public enum ContentLayout {
/// Arrange content horizontally with the traditional layout:
/// icon followed by title.
case horizontal
/// Arrange content horizontally with the reverse of the traditional
/// layout with the title followed by the icon.
case horizontalReversed
/// Arrange content vertically with the icon followed by the title.
case vertical
/// Arrange content vertically with the title followed by the icon.
case verticalReversed
}
That would allow us to specify horizontal buttons like the following:
And also vertical buttons like the following:
If we look at UIButton
there are methods that stand out as potential candidates we might implement to get icon and label layouts like we want:
open func contentRect(forBounds bounds: CGRect) -> CGRect
open func titleRect(forContentRect contentRect: CGRect) -> CGRect
open func imageRect(forContentRect contentRect: CGRect) -> CGRect
The first, contentRect(forBounds:)
, allows us to modify the location of the content of the button given the bounds of the UIView
. This is helpful if we want to draw a fancy border around the button. Honestly, you’re probably better off just setting the alignmentRectInsets
and allowing auto layout to figure this out for you however.
The other two — titleRect(forContentRect:)
and imageRect(forContentRect:)
— allow us to move the location of the title and image around within the content of the button. This seems like exactly what we what we want to do. However, there’s a catch: these methods don’t execute at all when we’re using auto layout — which is necessary to make multiline labels work. We’ll need an alternate solution.
By adding two method overrides to our button we can add breakpoints to determine what constraints are being added and when:
public override func addConstraint(_ constraint: NSLayoutConstraint) {
super.addConstraint(constraint)
}
public override func addConstraints(_ constraints: [NSLayoutConstraint]) {
super.addConstraints(constraints)
}
We discover all of the (questionable) constraints are added to our button in updateConstraints
. In fact, UIButton
inserts a couple new instances of a private _UIButtonContentCenteringSpacer
class to handle arranging the views. This isn’t how we’d probably do things today, but auto layout wasn’t quite as sophisticated when UIButton
first adopted it.
If we take a look at the view hierarchy at the start of updateConstraints
we see mostly what we expect:
<Button: 0x7f8c65e0fb50; baseClass = UIButton; frame = (150 114.5; 54 34)>
| <UIImageView: 0x7f8c65c0ef20; frame = (20 10; 14 14)>
| <UIButtonLabel: 0x7f8c65d306f0; frame = (1.5 6.5; 51 21); text = 'Button'>
However, immediately after updateConstraints
executes we can see the addition of the _UIButtonContentCenteringSpacer
views1:
<Button: 0x7f8c65e0fb50; baseClass = UIButton; frame = (150 114.5; 54 34)>
| <UIImageView: 0x7f8c65f05100; frame = (0 0; 54 34)>
| <UIImageView: 0x7f8c65c0ef20; frame = (20 10; 14 14)>
| <UIButtonLabel: 0x7f8c65d306f0; frame = (1.5 6.5; 51 21)>
| <_UIButtonContentCenteringSpacer: 0x7f8c65f2f790; frame = (0 0; 0 0); hidden = YES; tag = 12000274>
| <_UIButtonContentCenteringSpacer: 0x7f8c65f2fed0; frame = (0 0; 0 0); hidden = YES; tag = -12000274>
Apple’s documentation includes this warning about implementing an override for updateConstraints
:
This puts us in a somewhat tricky situation. We should always heed admonishments like this, however, the constraints UIButton
creates in its updateConstraints
aren’t quite fit for our purpose. Because my testing revealed all constraints were added in updateConstraints
I’d be tempted to simply skip the UIButton
implementation of updateConstraints
and call UIControl
directly.
The following bit of clever runtime manipulation is courtesy Dave DeLong, but I also received a bunch of help from the Seattle Xcoders group. Basically, we need to create a call to super
that bypasses UIButton
. We can do that using the runtime:
struct objc_super skipSuper;
skipSuper.receiver = self;
skipSuper.super_class = [[[self class] superclass] superclass];
void (*callSuper)(struct objc_super *, SEL) = (void (*)(struct objc_super *, SEL))objc_msgSendSuper;
callSuper(&skipSuper, _cmd);
I’d need to translate this into Swift — which I suppose I’m capable of — or rewrite my button class in Objective-C — which would make me rather happy. But for an example of the right way to do things, this feels wrong somehow. :)
Instead, I’m tempted to allow UIButton
to create it’s awkward constraints on its empty titleLabel
and imageView
and create our own views instead. That means we’ll be losing the default support UIButton
gives us for highlighting the button when touches begin, displaying an alternate appearance when disabled, and possible some more things. To get this functionality which UIButton
offers out of the box, we’ll need to implement the following at least:
open override var isHighlighted: Bool {
didSet { … }
}
open var isEnabled: Bool {
didSet { … }
}
But I wouldn’t be surprised if we wind up having to implement some combination of the following as well to ensure everything works correctly:
open func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
open func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool
open func endTracking(_ touch: UITouch?, with event: UIEvent?)
open func cancelTracking(with event: UIEvent?)
This feels like a lot of work and maybe it’s not any different from just creating a subclass of UIControl
and doing everything from scratch. We’ll also want to make certain our button is accessible by setting the accessibilityLabel
at some point, but we were already doing that anyway.
This feels like a lot of work ahead. Not an unreasonable amount of work for a first-class UI component, but certainly not something you’d want to knock out in an evening. But if your application calls for buttons of this kind, it would definitely make sense to invest a moderate amount of time to create a reusable component like this.
-
Did you spot the
tag
value? I certainly did. How many times have you been told not to useviewWithTag
because it performs a recursive search of the view hierarchy? I mean you should still avoid it, but really, I have to wonder why not just add two additional view pointers? ↩︎