Angled Gradient Layer
Gradient Start & End Points for Any Angle
Swift 4.2, Xcode 10.0
Given any angle, my code will set the respective start point and end point of a gradient layer.
If an angle greater than 360° is inputted, it will use the remainder when divided by 360.
- An input of 415° would yield the same result as an input of 55°
If an angle less than 0° is inputted, it will reverse the clockwise direction of rotation
- An input of -15° would yield the same result as an input of 345°
Code:
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
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
}
}
/// Private function to aid with the math when calculating the gradient angle
private func tanx(_ ð½: CGFloat) -> CGFloat {
return tan(ð½ * CGFloat.pi / 180)
}
// Overloads
/// Sets the start and end points on a gradient layer for a given angle.
public func calculatePoints(for angle: Int) {
calculatePoints(for: CGFloat(angle))
}
/// Sets the start and end points on a gradient layer for a given angle.
public func calculatePoints(for angle: Float) {
calculatePoints(for: CGFloat(angle))
}
/// Sets the start and end points on a gradient layer for a given angle.
public func calculatePoints(for angle: Double) {
calculatePoints(for: CGFloat(angle))
}
}
Usage:
let gradientLayer = CAGradientLayer()
// Setup gradient layer...
// Gradient Direction: →
gradient.calculatePoints(for: 0)
// Gradient Direction: ↗︎
gradient.calculatePoints(for: -45)
// Gradient Direction: ←
gradient.calculatePoints(for: 180)
// Gradient Direction: ↓
gradient.calculatePoints(for: 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.
If you are interested in how I figured it out, I made a table to visualize essentially what I am doing from 0° - 360°.
It looks like you forgot to set the startPoint
on your CAGradientLayer()
. The code below is the code you've provide, plus my addition.
import UIKit
class GradientView: UIView {
let gradientLayer = CAGradientLayer()
override func awakeFromNib() {
// 1
self.backgroundColor = ColorPalette.White
// 2
gradientLayer.frame = self.bounds
// 3
let color1 = ColorPalette.GrdTop.CGColor as CGColorRef
let color2 = ColorPalette.GrdBottom.CGColor as CGColorRef
gradientLayer.colors = [color1, color2]
//** This code should do the trick... **//
gradientLayer.startPoint = CGPointMake(0.0, 0.5)
// 4
gradientLayer.locations = [0.0, 1.0]
// 5
self.layer.addSublayer(gradientLayer)
}
}
You don't want to use locations
to specify the direction of the gradient. Instead use startPoint
and endPoint
for that.
The locations
array is used when one wants to specify where, in between startPoint
and endPoint
, the gradient should to take place. For example, if you want the colors to only take place in the middle 10% of the range from the start and end points, you'd use:
locations = [0.45, 0.55]
The locations
array doesn't dictate the direction. The startPoint
and endPoint
do. So, for a diagonal gradient from upper left to lower right, you would set startPoint
of CGPoint(x: 0, y: 0)
and an endPoint
to CGPoint(x: 1, y: 1)
.
For example:
@IBDesignable
class GradientView: UIView {
override class var layerClass: AnyClass { return CAGradientLayer.self }
private var gradientLayer: CAGradientLayer { return layer as! CAGradientLayer }
@IBInspectable var color1: UIColor = .white { didSet { updateColors() } }
@IBInspectable var color2: UIColor = .blue { didSet { updateColors() } }
override init(frame: CGRect = .zero) {
super.init(frame: frame)
configureGradient()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configureGradient()
}
private func configureGradient() {
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 1)
updateColors()
}
private func updateColors() {
gradientLayer.colors = [color1.cgColor, color2.cgColor]
}
}
E.g.
Note, unrelated to the immediate issue:
If you’re going to add the gradient as a sublayer, you want to update this sublayer’s
frame
inlayoutSubviews
so that as the view'sbounds
changes, so does theframe
of thegradientLayer
. But, better than that, override thelayerClass
of the view, and it will not only instantiate theCAGradientLayer
for you, but you also enjoy dynamic adjustments of the gradient as the view’s size changes, notably handling animated changes more gracefully.Likewise, I set
color1
andcolor2
such that they'll trigger an updating of the gradient, so that any changes in colors will be immediately reflected in the view.I made this
@IBDesignable
, so that if I drop this in its own framework, and then add theGradientView
in IB, I'll see the effect rendered in IB.
For Swift 2 implementation, see previous revision of this answer.