Crafting a beautiful, responsive UIButton in Swift
Most apps do it wrong. Learn how to stand out!
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, I’ll explain how to make it not only look beautiful, but also how to make it responsive, so that you can delight your 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)
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:
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
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
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:
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
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 me!
5. Proper dark mode support
I’ve been using the Kickstarter helper function to generate the 1px background images for a while, but starting with iOS 13 I’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?
Ugh, this is not good. To resolve this, I’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!
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 me 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 I can include it right below.
Further learning:
- “Nobody loves UIButton” by Jeff Watkins, an excellent series exploring UIButton in great detail.