diff --git a/Example/SwiftGradients/Base.lproj/Main.storyboard b/Example/SwiftGradients/Base.lproj/Main.storyboard
index c94a02c..5adc8e7 100644
--- a/Example/SwiftGradients/Base.lproj/Main.storyboard
+++ b/Example/SwiftGradients/Base.lproj/Main.storyboard
@@ -25,11 +25,20 @@
+
+
+
diff --git a/Example/SwiftGradients/ViewController.swift b/Example/SwiftGradients/ViewController.swift
index 05f344b..600731d 100644
--- a/Example/SwiftGradients/ViewController.swift
+++ b/Example/SwiftGradients/ViewController.swift
@@ -3,11 +3,44 @@ import SwiftGradients
class ViewController: UIViewController {
+ var gradientLayer: CAGradientLayer!
+
override func viewDidLoad() {
super.viewDidLoad()
- view.addGradient(
+ gradientLayer = view.addGradient(
colors: [.beachBlue, .limeGreen],
direction: .bottomToTop
)
}
+
+ override func touchesEnded(_ touches: Set, with event: UIEvent?) {
+ let randomChange = arc4random_uniform(4)
+ switch randomChange {
+ case 0:
+ self.gradientLayer.uiColors = [UIColor.random, UIColor.random]
+ case 1:
+ let initialStop = Int(arc4random_uniform(100))
+ gradientLayer.percentLocations = [
+ initialStop,
+ initialStop + Int(arc4random_uniform(UInt32(100 - initialStop)))
+ ]
+ case 2:
+ gradientLayer.direction = .rightToLeft
+ case 3:
+ gradientLayer.angle = CGFloat(arc4random_uniform(360))
+ default:
+ break
+ }
+ }
+}
+
+extension UIColor {
+ static var random: UIColor {
+ return UIColor(
+ red: CGFloat(arc4random_uniform(255)) / 255,
+ green: CGFloat(arc4random_uniform(255)) / 255,
+ blue: CGFloat(arc4random_uniform(255)) / 255,
+ alpha: 1
+ )
+ }
}
diff --git a/Sources/CAGradientLayerExtension.swift b/Sources/CAGradientLayerExtension.swift
index 48fe945..32ce055 100644
--- a/Sources/CAGradientLayerExtension.swift
+++ b/Sources/CAGradientLayerExtension.swift
@@ -11,76 +11,125 @@ import Foundation
import UIKit
extension CAGradientLayer {
- class func startPointFor(_ angle: Double) -> CGPoint {
- if let defaultDirection = GradientDirection(rawValue: angle) {
- switch defaultDirection {
- case .topToBottom:
- return CGPoint(x: 0.5, y: 0.0)
- case .bottomToTop:
- return CGPoint(x: 0.5, y: 1.0)
- case .leftToRight:
- return CGPoint(x: 0.0, y: 0.5)
- default:
- return CGPoint(x: 1.0, y: 0.5)
+ //MARK: Attributes accessors
+
+ /// Collection of UIColors used in the gradient.
+ public var uiColors: [UIColor] {
+ get {
+ guard let anyColors = colors else { return [] }
+ return anyColors.map { color in
+ CFGetTypeID(color as CFTypeRef) == CGColor.typeID ?
+ UIColor(cgColor: color as! CGColor) :
+ UIColor.clear
}
}
- return pointWithAngle(angle)
+ set {
+ colors = newValue.map { $0.cgColor }
+ }
}
-
- class func endPointFor(_ angle: Double) -> CGPoint {
- if let defaultDirection = GradientDirection(rawValue: angle) {
- switch defaultDirection {
- case .topToBottom:
- return CGPoint(x: 0.5, y: 1.0)
- case .bottomToTop:
- return CGPoint(x: 0.5, y: 0.0)
- case .leftToRight:
- return CGPoint(x: 1.0, y: 0.5)
- default:
- return CGPoint(x: 0.0, y: 0.5)
+
+ /// Color stop locations in percentages.
+ public var percentLocations: [Int] {
+ get {
+ guard let decimalLocations = locations else { return [] }
+ return decimalLocations.map { Int(exactly: $0.floatValue * 100) ?? 0 }
+ }
+ set{
+ locations = newValue.map { NSNumber(value: Float($0) / 100.0) }
+ }
+ }
+
+ /// Predefined gradient direction if specified or if the current angle
+ /// matches any of the GradientDirection cases.
+ public var direction: GradientDirection? {
+ get {
+ if
+ let direction = GradientDirection.allCases.first(where: { direction in
+ abs(direction.startPoint.x - startPoint.x) <= .ulpOfOne &&
+ abs(direction.startPoint.y - startPoint.y) <= .ulpOfOne &&
+ abs(direction.endPoint.x - endPoint.x) <= .ulpOfOne &&
+ abs(direction.endPoint.y - endPoint.y) <= .ulpOfOne
+ })
+ {
+ return direction
}
+ return nil
+ }
+ set {
+ guard let newDirection = newValue else { return }
+ startPoint = newDirection.startPoint
+ endPoint = newDirection.endPoint
+ }
+ }
+
+ /// The gradient angle in degrees, measured clockwise and starting at the left.
+ /// 0 -> left, 90 -> up, etc
+ public var angle: CGFloat {
+ get {
+ let product = endPoint.y - startPoint.y
+ let determinant = endPoint.x - startPoint.x
+ var degrees = CGFloat(atan2(product, determinant)) * 180 / CGFloat.pi
+ if degrees < 0 { degrees = 360 + degrees }
+
+ return degrees.truncatingRemainder(dividingBy: 360)
}
+ set {
+ startPoint = CAGradientLayer.startPointFor(newValue)
+ endPoint = CAGradientLayer.endPointFor(newValue)
+ }
+ }
+
+ //MARK: Angle and points helpers
+
+ class func startPointFor(_ angle: CGFloat) -> CGPoint {
+ return pointWithAngle(angle)
+ }
+
+ class func endPointFor(_ angle: CGFloat) -> CGPoint {
return pointWithAngle(angle, isStartPoint: false)
}
- /// **pointWithAngle**: Helper for CAGradientLayer's start and endPoint given an angle in degrees
- /// - Parameter **angle** The desired angle in degrees and measured anti-clockwise.
+ /// **pointWithAngle**: Helper for CAGradientLayer's start and endPoint
+ /// given an angle in degrees
+ /// - Parameter **angle** The desired angle in degrees, measured clockwise
+ /// and starting at the left.
/// - Parameter **isStartPoint** A boolean indicating which point you need.
- /// - Returns: The initial or ending CGPoint for a CAGradientLayer within the Unit Cordinate System.
+ /// - Returns: The initial or ending CGPoint for a CAGradientLayer
+ /// within the Unit Cordinate System.
private class func pointWithAngle(
- _ angle: Double,
+ _ angle: CGFloat,
isStartPoint: Bool = true
) -> CGPoint {
- // negative angles not allowed
- var positiveAngle = angle < 0 ? angle * -1.0 : angle
- var y1: Double, y2: Double, x1: Double, x2: Double
+ var ang = (-angle).truncatingRemainder(dividingBy: 360)
+ if ang < 0 { ang = 360 + ang }
+ let n: CGFloat = 0.5
- if // ranges when we know Y values
- (positiveAngle >= 45 && positiveAngle <= 135) ||
- (positiveAngle >= 225 && positiveAngle <= 315)
- {
- y1 = positiveAngle < 180 ? 0.0 : 1.0
- y2 = 1.0 - y1 //opposite to start Y
- x1 = positiveAngle >= 45 && positiveAngle <= 135 ?
- 1.5 - positiveAngle / 90 :
- abs(2.5 - positiveAngle / 90)
- x2 = 1.0-x1 //opposite to start X
- } else { // ranges when we know X values
- x1 = positiveAngle < 45 || positiveAngle >= 315 ? 1.0 : 0.0
- x2 = 1.0 - x1
- if positiveAngle > 135 && positiveAngle < 225 {
- y2 = abs(2.5 - positiveAngle / 90)
- y1 = 1.0 - y2
- } else { // Range 0-45 315-360
- //Turn this ranges into one single 90 degrees range
- positiveAngle = positiveAngle >= 0 && positiveAngle <= 45 ?
- 45.0 - positiveAngle :
- 360 - positiveAngle + 45
- y1 = positiveAngle / 90
- y2 = 1.0 - y1
- }
+ switch ang {
+ case 0...45, 315...360:
+ return isStartPoint ?
+ CGPoint(x: 0, y: n * tanx(ang) + n) :
+ CGPoint(x: 1, y: n * tanx(-ang) + n)
+ case 45...135:
+ return isStartPoint ?
+ CGPoint(x: n * tanx(ang - 90) + n, y: 1) :
+ CGPoint(x: n * tanx(-ang - 90) + n, y: 0)
+ case 135...225:
+ return isStartPoint ?
+ CGPoint(x: 1, y: n * tanx(-ang) + n) :
+ CGPoint(x: 0, y: n * tanx(ang) + n)
+ case 225...315:
+ return isStartPoint ?
+ CGPoint(x: n * tanx(-ang - 90) + n, y: 0) :
+ CGPoint(x: n * tanx(ang - 90) + n, y: 1)
+ default:
+ return isStartPoint ?
+ CGPoint(x: 0, y: n) :
+ CGPoint(x: 1, y: n)
}
- return isStartPoint ? CGPoint(x: x1, y: y1) : CGPoint(x: x2, y: y2)
+ }
+
+ private class func tanx(_ 𝜽: CGFloat) -> CGFloat {
+ return tan(𝜽 * CGFloat.pi / 180)
}
}
diff --git a/Sources/CALayerGradientsExtension.swift b/Sources/CALayerGradientsExtension.swift
index 62faa9a..9362263 100644
--- a/Sources/CALayerGradientsExtension.swift
+++ b/Sources/CALayerGradientsExtension.swift
@@ -29,7 +29,7 @@ public extension CALayer {
@discardableResult
func addGradient(
colors: [UIColor],
- angle: Double,
+ angle: CGFloat,
locations: [Int] = []
) -> CAGradientLayer {
return addGradient(
@@ -57,12 +57,12 @@ public extension CALayer {
insertSublayer(gradient, at: 0)
}
gradient.frame = bounds
- gradient.colors = colors.map { $0.cgColor }
+ gradient.uiColors = colors
gradient.startPoint = startPoint
gradient.endPoint = endPoint
if !locations.isEmpty {
- gradient.locations = locations.map { NSNumber(value: Float($0) / 100.0) }
+ gradient.percentLocations = locations
}
return gradient
}
diff --git a/Sources/UIViewGradientsExtension.swift b/Sources/UIViewGradientsExtension.swift
index 631027e..0a332bc 100644
--- a/Sources/UIViewGradientsExtension.swift
+++ b/Sources/UIViewGradientsExtension.swift
@@ -10,11 +10,37 @@ import Foundation
#if canImport(UIKit)
import UIKit
-public enum GradientDirection: Double {
- case topToBottom = 90.0
- case bottomToTop = 270.0
- case leftToRight = 180.0
- case rightToLeft = 0.0
+public enum GradientDirection: CGFloat, CaseIterable {
+ case leftToRight = 0
+ case topToBottom = 90
+ case rightToLeft = 180
+ case bottomToTop = 270
+
+ var startPoint: CGPoint {
+ switch self {
+ case .topToBottom:
+ return CGPoint(x: 0.5, y: 0.0)
+ case .bottomToTop:
+ return CGPoint(x: 0.5, y: 1.0)
+ case .leftToRight:
+ return CGPoint(x: 0.0, y: 0.5)
+ case .rightToLeft:
+ return CGPoint(x: 1.0, y: 0.5)
+ }
+ }
+
+ var endPoint: CGPoint {
+ switch self {
+ case .topToBottom:
+ return CGPoint(x: 0.5, y: 1.0)
+ case .bottomToTop:
+ return CGPoint(x: 0.5, y: 0.0)
+ case .leftToRight:
+ return CGPoint(x: 1.0, y: 0.5)
+ case .rightToLeft:
+ return CGPoint(x: 0.0, y: 0.5)
+ }
+ }
}
public extension UIView {
@@ -35,7 +61,7 @@ public extension UIView {
@discardableResult
func addGradient(
colors: [UIColor],
- angle: Double,
+ angle: CGFloat,
locations: [Int] = []
) -> CAGradientLayer {
return addGradient(
@@ -73,7 +99,7 @@ public extension Array where Element: UIView {
}
}
- func addGradient(colors: [UIColor], angle: Double, locations: [Int] = []) {
+ func addGradient(colors: [UIColor], angle: CGFloat, locations: [Int] = []) {
for view in self {
view.addGradient(colors: colors, angle: angle, locations: locations)
}
diff --git a/SwiftGradientsTests/SwiftGradientsTests.swift b/SwiftGradientsTests/SwiftGradientsTests.swift
index 57d0054..d1f0837 100644
--- a/SwiftGradientsTests/SwiftGradientsTests.swift
+++ b/SwiftGradientsTests/SwiftGradientsTests.swift
@@ -65,19 +65,88 @@ class SwiftGradientsTests: XCTestCase {
func testGradientAngleCalculation() {
let colors: [UIColor] = [.yellow, .green]
var gradientLayer = view.addGradient(colors: colors, angle: 90)
- XCTAssert(gradientLayer.startPoint == CGPoint(x: 0.5, y: 0))
- XCTAssert(gradientLayer.endPoint == CGPoint(x: 0.5, y: 1))
+ XCTAssertEqual(gradientLayer.startPoint.x, 0.5, accuracy: .ulpOfOne)
+ XCTAssertEqual(gradientLayer.endPoint.x, 0.5, accuracy: .ulpOfOne)
+ XCTAssert(gradientLayer.startPoint.y == 0)
+ XCTAssert(gradientLayer.endPoint.y == 1)
gradientLayer = view.addGradient(colors: colors, angle: 270)
- XCTAssert(gradientLayer.startPoint == CGPoint(x: 0.5, y: 1))
- XCTAssert(gradientLayer.endPoint == CGPoint(x: 0.5, y: 0))
+ XCTAssertEqual(gradientLayer.startPoint.x, 0.5, accuracy: .ulpOfOne)
+ XCTAssertEqual(gradientLayer.endPoint.x, 0.5, accuracy: .ulpOfOne)
+ XCTAssert(gradientLayer.startPoint.y == 1)
+ XCTAssert(gradientLayer.endPoint.y == 0)
gradientLayer = view.addGradient(colors: colors, angle: 180)
+ XCTAssertEqual(gradientLayer.startPoint.y, 0.5, accuracy: .ulpOfOne)
+ XCTAssertEqual(gradientLayer.endPoint.y, 0.5, accuracy: .ulpOfOne)
+ XCTAssert(gradientLayer.startPoint.x == 1)
+ XCTAssert(gradientLayer.endPoint.x == 0)
+
+ gradientLayer = view.addGradient(colors: colors, angle: 0)
XCTAssert(gradientLayer.startPoint == CGPoint(x: 0, y: 0.5))
XCTAssert(gradientLayer.endPoint == CGPoint(x: 1, y: 0.5))
+ }
+
+ //MARK: Accessors
+
+ func testUIColorAccessor() {
+ let locations = [50, 70]
+ let finalColors: [UIColor] = [.white, .black]
+ let gradientLayer = view.addGradient(
+ colors: [.blue, .red],
+ locations: locations
+ )
+ gradientLayer.uiColors = finalColors
+ let uiColors = gradientLayer.uiColors
+ XCTAssert(uiColors == finalColors)
+ }
+
+ func testPercentLocationsAccessor() {
+ let initialStop = Int(arc4random_uniform(100))
+ let locations = [
+ initialStop,
+ initialStop + Int(arc4random_uniform(UInt32(100 - initialStop)))
+ ]
+ let gradientLayer = view.addGradient(colors: [.white, .black])
+ gradientLayer.percentLocations = locations
+ XCTAssert(locations == gradientLayer.percentLocations)
+ }
+
+ func testDirectionAccessor() {
+ guard let direction = GradientDirection.allCases.randomElement() else {
+ XCTFail("Invalid gradient direction case.")
+ return
+ }
+
+ let gradientLayer = view.addGradient(colors: [.purple, .cyan])
+ gradientLayer.direction = direction
+ XCTAssert(direction == gradientLayer.direction)
- gradientLayer = view.addGradient(colors: colors, angle: 0)
- XCTAssert(gradientLayer.startPoint == CGPoint(x: 1, y: 0.5))
- XCTAssert(gradientLayer.endPoint == CGPoint(x: 0, y: 0.5))
+ gradientLayer.angle = 0
+ XCTAssert(gradientLayer.direction == .leftToRight)
+ gradientLayer.angle = 90
+ XCTAssert(gradientLayer.direction == .topToBottom)
+ gradientLayer.angle = 180
+ XCTAssert(gradientLayer.direction == .rightToLeft)
+ gradientLayer.angle = 270
+ XCTAssert(gradientLayer.direction == .bottomToTop)
+ gradientLayer.angle = 360
+ XCTAssert(gradientLayer.direction == .leftToRight)
+ }
+
+ func testAngleAccessor() {
+ let gradientLayer = view.addGradient(colors: [.brown, .orange])
+ gradientLayer.angle = 0
+ XCTAssert(gradientLayer.angle == 0)
+ gradientLayer.angle = 90
+ XCTAssertEqual(gradientLayer.angle, 90, accuracy: 0.001)
+ gradientLayer.angle = 180
+ XCTAssertEqual(gradientLayer.angle, 180, accuracy: 0.001)
+ gradientLayer.angle = 270
+ XCTAssertEqual(gradientLayer.angle, 270, accuracy: 0.001)
+ gradientLayer.angle = 360
+ XCTAssert(gradientLayer.angle == 0)
+ gradientLayer.angle = 450
+ XCTAssertEqual(gradientLayer.angle, 90, accuracy: 0.001)
}
}