How to add gradient to any view

08 February 2020

In this article I am going to show how to add a gradient to any view without protocols. In it we will use CAGradientLayer to draw the gradient; An extension of CALayer for finding the created layer so we can skip the assignment to a property; And an UIView extension for using and customizing the gradient. We are going to start by adding the CALayer extension to find our gradient layer later on:

import UIKit

// MARK: - Layer search
extension CALayer {

    /// Returns layer with the specified name.
    ///
    /// - Parameter layerName: Name of the layer to search for.
    /// - Returns: Layer with the specified name
    func sublayer(named layerName: String) -> CALayer? {
        guard let sublayers = sublayers else {
            return nil
        }

        for aLayer in sublayers {
            guard aLayer.name == layerName else {
                continue
            }

            return aLayer
        }

        return nil
    }

}

As you can see we are adding an additional method to CALayer that will search the sublayers of it. We are using the name variable that is present in CALayer. We are just going through all sublayers and checking if there is a name match. Keep in mind that this method will give us the first match of the name and it needs to be exact, even the casing.

Next we need to crate the extension of UIView and start drawing the gradient. For it we will need a constant for the name of the gradient. We can achieve that by making a private structure in our extension, because it will only be used inside it.

import UIKit

// MARK: - Gradient
extension UIView {

    private struct Constants {

        /// Name of the gradient layer.
        static let gradientLayerName = "gradientLayer"

    }

}

Now the making of the CAGradientLayer:

/// Layer responsible for the background gradient.
private var gradientLayer: CAGradientLayer {
    // Get the gradient layer from the view's sublayers.
    guard let gradient = layer.sublayer(named: Constants.gradientLayerName) as? CAGradientLayer else {
        // The gradient layer does not exist, create it.
        let gradient = CAGradientLayer()
        gradient.frame = bounds
        gradient.name = Constants.gradientLayerName
        gradient.startPoint = .zero
        gradient.zPosition = -1
        gradient.shouldRasterize = false

        layer.addSublayer(gradient)

        return gradient
    }

    return gradient
}

We are implementing just a getter for the gradientLayer which will give us the gradient layer that already exists or, if not, create it and return it. Here on creation, we are just setting the basics like:

  • The frame of the gradient which will fill the whole view in which we are going to add it, so we are setting its frame to the bounds of the UIView;
  • The name of the layer. This is needed to find it later on;
  • The starting point in the gradient. By default it will be (0, 0) which is the top left corner of the view, in Swift we are passing it as CGPoint.zero;
  • The z position, so it will be always below all other layers;
  • This shouldRasterize variable which will rasterize the drawn gradient every time it is changed. You can make it as a variable in the extension if you want, but I need it as a vector because I use a lot of scale animations in my project and if it is raster it will just pixelize for no reason;
  • And add our gradient layer to the main layer of the view.

It is time to add some getters and setters so we can customize the gradient however we like. We will start with the colors that will construct the gradient.

/// Colors used to create the gradient layer.
var gradientColors: [UIColor]? {
    get {
        return gradientLayer.colors?.compactMap { UIColor(cgColor: $0 as! CGColor) }
    }
    set {
        gradientLayer.colors = newValue?.compactMap { $0.cgColor }
    }
}

This is a getter and setter because sometimes we would need to check if we are working with the correct gradient. I prefer to work with the UI components so I do convert them to CGColor. This allows me to use the colors that I added in my .xcassets catalogue. The colors passed to this setter will be converted to CGColor and passed to the gradientLayer where they will be evenly distributed to make a beautiful gradient.

The last property that I want to change is the ending point of the gradient draw. You can pass it as CGPoint and also make a variable for the starting point which will allow you to draw diagonal gradients as well but I need them to be just vertical or horizontal. To make that I am going to add an enum for that.

/// Direction of the gradient
///
/// - horizontal: The gradient will be filled from left to right.
/// - vertical: The gradient will be filled from top to bottom
enum GradientDirection {
    case horizontal
    case vertical
}

/// Direction of the gradient.
var gradientDirection: GradientDirection {
    get {
        if gradientLayer.endPoint == CGPoint(x: 1, y: 0) {
            return .horizontal
        }

        return .vertical
    }
    set {
        switch newValue {
        case .horizontal:
            gradientLayer.endPoint = CGPoint(x: 1, y: 0)
        default:
            gradientLayer.endPoint = CGPoint(x: 0, y: 1)
        }
    }
}

If we want a horizontal gradient we just pass end point (1, 0) to the gradientLayer and as we have starting point when we created it of (0, 0) it will be drawn on the x axis. If we want a vertical gradient we just pass the point (0, 1).

How do we use it? It is super simple, just create your UIView or subclass of it like a UIButton and pass the colors and direction that you want for the gradient:

view.gradientColors = [.green, .blue] // left and right color
view.gradientDirection = .horizontal

Full snippet:

// Created by © 2020 SwiftEverywhere. Can be used free of charge.

import UIKit

// MARK: - Layer search
extension CALayer {

    /// Returns layer with the specified name.
    ///
    /// - Parameter layerName: Name of the layer to search for.
    /// - Returns: Layer with the specified name
    func sublayer(named layerName: String) -> CALayer? {
        guard let sublayers = sublayers else {
            return nil
        }

        for aLayer in sublayers {
            guard aLayer.name == layerName else {
                continue
            }

            return aLayer
        }

        return nil
    }

}

import UIKit

extension UIView {

    private struct Constants {

        /// Name of the gradient layer.
        static let gradientLayerName = "gradientLayer"

    }

    /// Direction of the gradient
    ///
    /// - horizontal: The gradient will be filled from left to right.
    /// - vertical: The gradient will be filled from top to bottom
    enum GradientDirection {
        case horizontal
        case vertical
    }

    /// Direction of the gradient.
    var gradientDirection: GradientDirection {
        get {
            if gradientLayer.endPoint == CGPoint(x: 1, y: 0) {
                return .horizontal
            }

            return .vertical
        }
        set {
            switch newValue {
            case .horizontal:
                gradientLayer.endPoint = CGPoint(x: 1, y: 0)
            default:
                gradientLayer.endPoint = CGPoint(x: 0, y: 1)
            }
        }
    }

    /// Colors used to create the gradient layer.
    var gradientColors: [UIColor]? {
        get {
            return gradientLayer.colors?.compactMap { UIColor(cgColor: $0 as! CGColor) }
        }
        set {
            gradientLayer.colors = newValue?.compactMap { $0.cgColor }
        }
    }

    /// Layer responsible for the background gradient.
    private var gradientLayer: CAGradientLayer {
        // Get the gradient layer from the view's sublayers.
        guard let gradient = layer.sublayer(named: Constants.gradientLayerName) as? CAGradientLayer else {
            // The gradient layer does not exist, create it.
            let gradient = CAGradientLayer()
            gradient.frame = bounds
            gradient.name = Constants.gradientLayerName
            gradient.startPoint = .zero
            gradient.zPosition = -1
            gradient.shouldRasterize = false

            layer.addSublayer(gradient)

            return gradient
        }

        return gradient
    }

}