How to fill a CAShapeLayer with an angled gradient

I think it's

shape.startPoint = CGPoint(x: 1.0, y: 0.0)
shape.endPoint = CGPoint(x: 0.0, y: 1.0)

, which is the first color at the bottom-right to the second color at the top-left. If you want the first color at the top-right and second color at the bottom-left, then you should have

shape.startPoint = CGPoint(x: 1.0, y: 1.0)
shape.endPoint = CGPoint(x: 0.0, y: 0.0)

First color at top-left, second color at bottom-right

shape.startPoint = NSMakePoint(x: 0.0, y: 1.0)
shape.endPoint = NSMakePoint(x: 1.0, y: 0.0)

first color at bottom-left, second color at top-right

shape.startPoint = CGPoint(x: 0.0, y: 0.0)
shape.endPoint = CGPoint(x: 1.0, y: 1.0)

Easily Apply Gradient to CALayer

Swift 4.2, Xcode 10.0

While the above solutions only really work with trivial angles like 45°, my code is able to set a gradient to any given angle.

public extension CALayer {

    public func applyGradient(of colors: UIColor..., atAngle angle: CGFloat) -> CAGradientLayer {
        let gradient = CAGradientLayer()
        gradient.frame = frame
        gradient.colors = colors
        gradient.calculatePoints(for: angle)
        gradient.mask = self
        return gradient
    }

}


public extension CAGradientLayer {

    /// Sets the start and end points on a gradient layer for a given angle.
    ///
    /// - Important:
    /// *0°* is a horizontal gradient from left to right.
    ///
    /// With a positive input, the rotational direction is clockwise.
    ///
    ///    * An input of *400°* will have the same output as an input of *40°*
    ///
    /// With a negative input, the rotational direction is clockwise.
    ///
    ///    * An input of *-15°* will have the same output as *345°*
    ///
    /// - Parameters:
    ///     - angle: The angle of the gradient.
    ///
    public func calculatePoints(for angle: CGFloat) {


        var ang = (-angle).truncatingRemainder(dividingBy: 360)

        if ang < 0 { ang = 360 + ang }

        let n: CGFloat = 0.5

        let tanx: (CGFloat) -> CGFloat = { tan($0 * CGFloat.pi / 180) }

        switch ang {

        case 0...45, 315...360:
            let a = CGPoint(x: 0, y: n * tanx(ang) + n)
            let b = CGPoint(x: 1, y: n * tanx(-ang) + n)
            startPoint = a
            endPoint = b

        case 45...135:
            let a = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
            let b = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
            startPoint = a
            endPoint = b

        case 135...225:
            let a = CGPoint(x: 1, y: n * tanx(-ang) + n)
            let b = CGPoint(x: 0, y: n * tanx(ang) + n)
            startPoint = a
            endPoint = b

        case 225...315:
            let a = CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
            let b = CGPoint(x: n * tanx(ang - 90) + n, y: 1)
            startPoint = a
            endPoint = b

        default:
            let a = CGPoint(x: 0, y: n)
            let b = CGPoint(x: 1, y: n)
            startPoint = a
            endPoint = b

        }
    }

}

Usage:

let layer = CAShapeLayer()

// Setup layer...

// Gradient Direction: →
let gradientLayer1 = layer.applyGradient(of: UIColor.yellow, UIColor.red, at: 0)

// Gradient Direction: ↗︎
let gradientLayer2 = layer.applyGradient(of: UIColor.purple, UIColor.yellow, UIColor.green, at: -45)

// Gradient Direction: ←
let gradientLayer3 = layer.applyGradient(of: UIColor.yellow, UIColor.blue, UIColor.green, at: 180)

// Gradient Direction: ↓
let gradientLayer4 = layer.applyGradient(of: UIColor.red, UIColor.blue, at: 450)

Mathematical Explanation

So I actually just recently spent a lot of time trying to answer this myself. Here are some example angles just to help understand and visualize the clockwise direction of rotation.

Example Angles

If you are interested in how I figured it out, I made a table to visualize essentially what I am doing from - 360°.

Table


Why not use CAGradientLayer which has startPoint and endPoint properties.

You can do:

import UIKit
import PlaygroundSupport

let frame = CGRect(x: 0, y: 0, width: 100, height: 100)
let view = UIView(frame: frame)

PlaygroundPage.current.liveView = view

let path = UIBezierPath(ovalIn: frame)

let shape = CAShapeLayer()
shape.frame = frame
shape.path = path.cgPath
shape.fillColor = UIColor.blue.cgColor

let gradient = CAGradientLayer()
gradient.frame = frame
gradient.colors = [UIColor.blue.cgColor,
                   UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
gradient.mask = shape

view.layer.addSublayer(gradient)

enter image description here

Note: Added a bezier path for a circle, because it would work without the mask for the square.