Crafting a beautiful, responsive UIButton in Swift

Most apps do it wrong. Learn how to stand out!

Oleg Dreyman
6 min readJan 24, 2021

UIButton is one of the most loved and hated controls in UIKit. It’s central to pretty much any app out there, but it being one of the oldest UIKit classes, it sometimes feel quite awkward to use. In this article, we’ll explain how to make it not only look beautiful, but also how to make it responsive, so that we can delight our users (spoiler: it’s actually not hard at all!). This is how I use UIButton in all of my projects, so let’s dive in!

TL;DR? See this gist.

1. “Vanilla” UIButton is very bare bones

Well, let’s start playing. First, what happens if we simply create a “vanilla” UIButton with no configuration? Not much:

let button = UIButton()
button.tintColor = .systemTeal
button.setTitle("Tap Me", for: .normal)
Button with blue text color and no background color

It has no background, and, surprisingly, there is no straightforward way to add it! This has been the case since iOS 7, when Apple decided that the buttons should now only use colors to differentiate buttons from labels. Which is fine, but it’s fair to assume that most apps would want at least some of their buttons to be bolder, more vibrant, more beautiful. Something like this:

Button with white text color and blue background color with rounded corners

Want to now how to achieve this? Keep going!

2. Adding background color to a button

In order to add a background color to our vanilla UIButton, we will have to use setBackgroundImage(_:for:) function. As you can see, we will need UIImage object to use it. So we’ll use this helper function (from the Kickstarter team) to generate one pixel images with the given color, that we will then use to set as background to our buttons:

extension UIImage {
public static func pixel(ofColor color: UIColor) -> UIImage {
let pixel = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)

UIGraphicsBeginImageContext(pixel.size)
defer { UIGraphicsEndImageContext() }

guard let context = UIGraphicsGetCurrentContext() else { return UIImage() }

context.setFillColor(color.cgColor)
context.fill(pixel)

return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}
}

Good! Now let’s apply this to our UIButton and see what happens:

let button = UIButton()
button.setTitle("Tap Me", for: .normal)
button.tintColor = .white
button.setBackgroundImage(.pixel(ofColor: .systemBlue), for: .normal)
// Auto Layout code, for reference:
view.addSubview(button)
button.translatesAutoresizingMaskIntoConstraints = false
button.heightAnchor.constraint(equalToConstant: 56).isActive = true
button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24).isActive = true
button.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24).isActive = true
button.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
Button with white text color and blue background color, no rounded corners

Okay, this is already better! Let’s add this functionality to an extension for convenience:

extension UIButton {
func setBackgroundColor(_ backgroundColor: UIColor, for state: UIControl.State) {
self.setBackgroundImage(.pixel(ofColor: backgroundColor), for: state)
}
}

button.setBackgroundColor(.systemBlue, for: .normal)

“Rectangle” button doesn’t always look great though. Let’s continue by adding some rounded corners.

3. Adding rounded corners

Well this one is very simple. We will use .layer.cornerRadius property that’s available for any UIView subclass:

let button = UIButton()
button.setTitle("Tap Me", for: .normal)
button.tintColor = .white
button.setBackgroundColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 20
button.layer.masksToBounds = true
Button with white text color and blue background color, and rounded corners

Lovely! Surely, you can play with the exact number to achieve the corner radius you want. Don’t forget to set .layer.masksToBounds to true!

But we’re not quite done yet! Why? Well, let’s try to play with our button a little bit:

Button with white text color and blue background color. When tapped or highlighted, changes color, but with no animation

Well, it looks okay, and in fact most apps will just leave it like this. But we can actually do much better, with almost no effort!

4. Making the button more responsive

The secret lies in something called UIButton.ButtonType. When you simply create a button using UIButton() initializer, it will create it with .custom button type. However, by using .system button type you will get a much more fluid, playful and responsive animation! Just check it out, it’s magical:

let button = UIButton(type: .system)
button.setTitle("Tap Me", for: .normal)
button.tintColor = .white
button.setBackgroundColor(.systemBlue, for: .normal)
button.layer.cornerRadius = 20
button.layer.masksToBounds = true
Button with white text color and blue background color. When tapped or highlighted, changes color with beautiful animation

This feels so, so much better. It gets especially clear once you use this on a physical device. It’s very intuitive — it animates fluidly depending on whether you’re quickly tapping it or holding your finger on it. Once you experience this, you can never go back, trust us!

5. Proper dark mode support

We’ve been using the Kickstarter helper function to generate our 1px background images for a while, but starting with iOS 13 we’ve noticed some issues when switching from light mode to dark mode or vice versa. Let’s say we have a button that should have black background & white text in light mode, and white background & black text in dark mode:

let textColor = UIColor { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? .black : .white
}
let backgroundColor = UIColor { (trait) -> UIColor in
return trait.userInterfaceStyle == .dark ? .white : .black
}
let button = UIButton(type: .system)
button.tintColor = textColor
button.setBackgroundColor(backgroundColor, for: .normal)

What will happen if we change from light mode to dark mode while the app is running?

Text color changes to black when dark mode is on, but the background stays black, so the text is not visible

Ugh, this is not good. To resolve this, we’ve modified Kickstarter’s helper function to generate 1px background images with proper light mode and dark mode support (feel free to simply copy and paste):

extension UIImage {

static func pixel(ofColor color: UIColor) -> UIImage {
let lightModeImage = UIImage.generatePixel(ofColor: color, userInterfaceStyle: .light)
let darkModeImage = UIImage.generatePixel(ofColor: color, userInterfaceStyle: .dark)
lightModeImage.imageAsset?.register(darkModeImage, with: UITraitCollection(userInterfaceStyle: .dark))
return lightModeImage
}

static private func generatePixel(ofColor color: UIColor, userInterfaceStyle: UIUserInterfaceStyle) -> UIImage {
var image: UIImage!
if #available(iOS 13.0, *) {
UITraitCollection(userInterfaceStyle: userInterfaceStyle).performAsCurrent {
image = .generatePixel(ofColor: color)
}
}
else {
image = .generatePixel(ofColor: color)
}
return image
}

static private func generatePixel(ofColor color: UIColor) -> UIImage {
let pixel = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)

UIGraphicsBeginImageContext(pixel.size)
defer { UIGraphicsEndImageContext() }

guard let context = UIGraphicsGetCurrentContext() else {
return UIImage()
}

context.setFillColor(color.cgColor)
context.fill(pixel)

return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage()
}
}

Now it works flawlessly!

Text color changes to black when dark mode is on, and the background changes to white, so the text is visible

I’ve been using this modified version in all my recent projects, and haven’t had any problems with it.

TL;DR?

Check out our GitHub gist if you want to have all the code in one place.

Thanks for reading this story! Don’t hesitate to ask or suggest anything in the “responses” section below. You can also contact us on Twitter or find us on GitHub. If you have written a piece (or stumbled upon one) exploring similar topic — make sure to post a link to it in the comments so that we can include it right below.

Further learning:

--

--

Oleg Dreyman

iOS development know-it-all. Talk to me about Swift, coffee, photography & motorsports.