Swift: Create a UIButton with background and border gradients, rounded corners and a shadow

The final button

The standard UIButton is a fairly bland affair. We don’t need to write and inordinate amount of code to spruce it up though. In this tutorial we’ll look at how to add background and border gradients, round its corners and display a drop shadow underneath it.

Note: If you just want the code and aren’t bothered about the explanations, you’ll find a complete subclass at the bottom of this tutorial.

Setup

We’re going to be styling our UIButton via a subclass so that the design can be trivially reused and updated throughout the app.

Start by creating a new “Cocoa Touch Class” file:

  • Class: I’m naming mine PrimaryButton. You can name yours whatever you want, just make sure you replace PrimaryButton with whichever name you chose as you follow along.
  • Subclass of: UIButton
  • Language: Swift

Next, add a button object to the ViewController of your choosing. With the button selected, open the Identity Inspector tab in the sidebar and change the button’s class to PrimaryButton.

That’s all the setup done. We can now get on with the styling.

Styling

We’re going to be working in PrimaryButton.swift for the remainder of this tutorial so open that up now. You can delete the commented out draw method.

All of our modifications to the generic UIButton are going to be done in the layoutSubviews method, so start by overriding that in the class.

override func layoutSubviews() {
    super.layoutSubviews()

    // All remaining snippets go here…
}

The border gradient

We’ll start by implementing the border gradient as all our other additions are going to be placed below it. There are two parts to a border gradient: the first is the CAGradientLayer that, as you might expect, draws the gradient; the second is a mask that defines where the gradient will be visible. If we didn’t create a mask, the gradient layer would take up the whole of the button.

The first portion of the following snippet draws the aforementioned CAGradientLayer; the second portion defines the mask. It’s the lineWidth property on borderMask that determines how thick the stroke around the button will be.

I think frame is the only property here that isn’t self-explanatory. We’ll be seeing it throughout this post so we might as well explain it here. frame simply defines the position and size of a layer. Here, borderGradient.frame = self.bounds translates to “set the position and size values of borderGradient to be the same as the position and size values of the button”.

// Border:

let borderGradientTop = UIColor(hue:0.72, saturation:0.59, brightness:0.90, alpha:1.00).cgColor
let borderGradientBottom = UIColor(hue:0.72, saturation:0.82, brightness:0.90, alpha:1.00).cgColor
let borderGradient = CAGradientLayer()
borderGradient.frame = self.bounds
borderGradient.colors = [borderGradientTop, borderGradientBottom]
borderGradient.cornerRadius = 4

let borderMask = CAShapeLayer()
borderMask.lineWidth = 1
borderMask.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 4).cgPath
borderMask.fillColor = UIColor.clear.cgColor
borderMask.strokeColor = UIColor.black.cgColor

borderGradient.mask = borderMask
self.layer.insertSublayer(borderGradient, below: self.titleLabel?.layer)

// Next snippet goes here…

The background gradient

The code for the background is almost an exact copy of the first portion of the above snippet, we just need to change the colours.

// Background:
let backgroundGradientTop = UIColor(hue:0.72, saturation:0.58, brightness:0.97, alpha:1.00).cgColor
let backgroundGradientBottom = UIColor(hue:0.72, saturation:0.80, brightness:0.96, alpha:1.00).cgColor
let background = CAGradientLayer()
background.frame = self.bounds
background.colors = [backgroundGradientTop, backgroundGradientBottom]
background.cornerRadius = 4
self.layer.insertSublayer(background, below: borderGradient)

// Next snippet goes here…

The shadow

We can add the shadow via the UIButton’s layer property. No need to create another sublayer.

Whilst not particularly intuitive, the width and height values of shadowOffset are in fact what control the shadow’s x and y positions.

// Shadow:
self.layer.shadowColor = UIColor.black.cgColor
self.layer.shadowOffset = CGSize(width: 0, height: 0.5)
self.layer.shadowOpacity = 0.2
self.layer.shadowRadius = 1
self.layer.masksToBounds = false

// Next snippet goes here…

The text

If you try running your app now, the button should look great… except for the text. Let’s finish up here by polishing that.

// Text:
self.setTitleColor(UIColor.white, for: .normal)
self.setTitle(self.titleLabel?.text?.uppercased(), for: .normal)
self.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold)

Final Subclass

Here’s what your final class should look like:

import UIKit

class PrimaryButton: UIButton {

    override func layoutSubviews() {
        super.layoutSubviews()

        // Border:

        let borderGradientTop = UIColor(hue:0.72, saturation:0.59, brightness:0.90, alpha:1.00).cgColor
        let borderGradientBottom = UIColor(hue:0.72, saturation:0.82, brightness:0.90, alpha:1.00).cgColor
        let borderGradient = CAGradientLayer()
        borderGradient.frame = self.bounds
        borderGradient.colors = [borderGradientTop, borderGradientBottom]
        borderGradient.cornerRadius = 4

        let borderMask = CAShapeLayer()
        borderMask.lineWidth = 1
        borderMask.path = UIBezierPath(roundedRect: self.bounds, cornerRadius: 4).cgPath
        borderMask.fillColor = UIColor.clear.cgColor
        borderMask.strokeColor = UIColor.black.cgColor

        borderGradient.mask = borderMask
        self.layer.insertSublayer(borderGradient, below: self.titleLabel?.layer)

        // Background:
        let backgroundGradientTop = UIColor(hue:0.72, saturation:0.58, brightness:0.97, alpha:1.00).cgColor
        let backgroundGradientBottom = UIColor(hue:0.72, saturation:0.80, brightness:0.96, alpha:1.00).cgColor
        let background = CAGradientLayer()
        background.frame = self.bounds
        background.colors = [backgroundGradientTop, backgroundGradientBottom]
        background.cornerRadius = 4
        self.layer.insertSublayer(background, below: borderGradient)

        // Shadow:
        self.layer.shadowColor = UIColor.black.cgColor
        self.layer.shadowOffset = CGSize(width: 0, height: 0.5)
        self.layer.shadowOpacity = 0.2
        self.layer.shadowRadius = 1
        self.layer.masksToBounds = false

        // Text
        self.setTitleColor(UIColor.white, for: .normal)
        self.setTitle(self.titleLabel?.text?.uppercased(), for: .normal)
        self.titleLabel?.font = UIFont.systemFont(ofSize: 12, weight: .bold)
    }

}

I tried to keep things as simple as possible here, but once you’ve got this working I’d recommend creating a variable at the top of the function to manage all of the cornerRadius properties.

Conclusion

I hope this was of use to you. I’m still a huge fan of “buttons that look like buttons” both from an aesthetics standpoint and, more importantly, from a UX standpoint. If this tutorial helps transform even one UIButton back into a “button that looks like a button” then I’ll be happy.

Next time we’ll look at how to update the button’s design as its state changes.