Constraints and UIButton

We’re accustomed to the leading-to-trailing layout of UIButton, but what if we want to break from the everyday and put our image on top of the text or after the text?

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:

image-20200407073922753

And also vertical buttons like the following:

image-20200407073958596

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:

image-20200407084922973

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.


  1. Did you spot the tag value? I certainly did. How many times have you been told not to use viewWithTag 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? ↩︎