Dressing up your UIButton

Jony Ive didn’t take all the fun out of buttons with iOS 7; you can have stylish buttons, the power is still there, but you have to be willing to make the effort.

Before we start decorating our buttons with faux leather backgrounds reminiscent of the days before iOS 7, lets take a moment to see how our buttons behave for users with vision limitations. By default, Apple would exempt buttons from scaling along with the text of your UI. I think this is a mistake. You can change this behaviour quite easily:

button.titleLabel?.adjustsFontForContentSizeCategory = true

However, in the previous example with multiline buttons, this won’t work because the fonts are embedded in the NSAttributedString. We’ll need to do a tiny bit more work. Really, it’s nothing.

public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
    guard self.traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory else { return }

    self.updateTitle()
}

So when the trait collection changes, if we notice that the preferred content size category has changed, we update the title. I suppose I could have been smarter by just updating the fonts in the NSAttributedString, but to be honest, most folks don’t change their type size. They set it and go. This will work just fine.

Multiline button at AX4 size

Now let’s take a look at bringing back the nice rounded button styles we’re starting to see in some iOS apps. There are a number of ways we could go about this, but in keeping with how I like to approach things, we’re going to take advantage of the features UIButton already offers rather than building something new.

Unfortunately, when we add a background to UIButton the code that adds spacing between the image and the title stops working. I’m not certain exactly why1, but you can see the result here:

rounded-background-button

In a future article I’m planning to show you how to move the image all around the button, so for now, I’ll simply remove the image leaving a nice (non-broken) button with a rounded background image:

rounded-background-button

I’m going to get a little tricky with this button and overload the meaning of the backgroundColor property. Instead of backgroundColor determining the background colour of the button itself, I’m going to use that to drive the colour of the background image. To do this, I’ll override the backgroundColor property:

var _buttonBackgroundColor: UIColor?
public override var backgroundColor: UIColor? {
    get { _buttonBackgroundColor }
    set {
        _buttonBackgroundColor = newValue
        self.setupButton()
    }
}

Notice, I don’t ever call super here. So the button will never know it’s background colour has changed. Instead, I store the colour and call setupButton.

In the previous arcticle about UIButton, I failed to mention the setupButton method. This wasn’t out of any nefarious intent, but rather because setupButton did simple things like setting up constraints to ensure the titleLabel didn’t extend beyond the top or bottom of the button when it began to wrap.

if self.titleConstraints.isEmpty, let titleLabel = self.titleLabel {
    titleLabel.numberOfLines = 0
    titleConstraints.append(titleLabel.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor))
    titleConstraints.append(titleLabel.bottomAnchor.constraint(lessThanOrEqualTo: self.bottomAnchor))
    NSLayoutConstraint.activate(titleConstraints)
}

But now I need to add a bit more code in there to handle configuring the background image based on the new background colour:

if let backgroundColor = self.backgroundColor {
    self.contentEdgeInsets = UIEdgeInsets(top: self.cornerRadius / 2.0,
                                          left: self.cornerRadius,
                                          bottom: self.cornerRadius / 2.0,
                                          right: self.cornerRadius)
    let backgroundImage = self.backgroundImage(fill: backgroundColor)
    let highlightImage = self.backgroundImage(fill: backgroundColor.withAlphaComponent(0.3))
    self.setBackgroundImage(backgroundImage, for: UIControl.State.normal)
    self.setBackgroundImage(highlightImage, for: UIControl.State.highlighted)
} else {
    self.setBackgroundImage(nil, for: UIControl.State.normal)
}

This background image is created using the following function which takes a fill colour and returns a resizeable image based on the cornerRadius of the button.

func backgroundImage(fill fillColor: UIColor) -> UIImage {
    let radius = self.cornerRadius
    let size = CGSize(width: radius * 2 + 1, height: radius * 2 + 1).floorToPixel()
    let rect = CGRect(origin: CGPoint.zero, size: size)

    let renderer = UIGraphicsImageRenderer(size: size)

    let backgroundImage = renderer.image { _ in
        let path = UIBezierPath(roundedRect: rect, cornerRadius: radius)
        fillColor.set()
        path.fill()
    }

    return backgroundImage.resizableImage(withCapInsets: UIEdgeInsets(all: radius))
}

Unfortunately, this code has one flaw: at large accessibility sizes, the text bumps up against the side of the background image as you can see here:

rounded-button-ax4

To fix this we’ll add some constraints to the titleLabel to ensure it respects the contentEdgeInsets and keeps a nice amount of padding around the button:

if self.currentBackgroundImage != nil {
    if let titleLabel = self.titleLabel, self.backgroundConstraints.isEmpty {
        self.backgroundConstraints = [
            titleLabel.leadingAnchor.constraint(greaterThanOrEqualTo: self.leadingAnchor,
                                                constant: self.cornerRadius),
            titleLabel.topAnchor.constraint(greaterThanOrEqualTo: self.topAnchor,
                                            constant: self.cornerRadius / 2.0),
            self.trailingAnchor.constraint(greaterThanOrEqualTo: titleLabel.trailingAnchor,
                                           constant: self.cornerRadius),
            self.bottomAnchor.constraint(greaterThanOrEqualTo: titleLabel.bottomAnchor,
                                         constant: self.cornerRadius / 2.0)
        ]
        NSLayoutConstraint.activate(self.backgroundConstraints)
    }
} else {
    if !self.backgroundConstraints.isEmpty {
        NSLayoutConstraint.deactivate(self.backgroundConstraints)
        self.backgroundConstraints = []
    }
}

These constraints ensure our button looks perfect even at accessibility sizes.

rounded-button-ax4-correct

We don’t build skeuomorphic apps any more, but that doesn’t mean we can’t have nice things. Building a great app has always meant putting in a bit more work than just putting standard controls on the screen and hoping for the best. Adding a rounded background to your buttons is no different. But just like adding multiline support, do it once and you’ll have that functionality across your entire app.


  1. Yes, I know I should file a Feedback and maybe I even will. But ever since Apple switched from Radar to Feedback it’s felt like even more of a black hole. Although I have to say the iOS app for filing feedback is definitely much nicer than the old web site. ↩︎