Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/gradient layer accessors #3

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Example/SwiftGradients/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,20 @@
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Tap to change gradient" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KqG-SY-hI1">
<rect key="frame" x="114" y="639" width="147" height="18"/>
<fontDescription key="fontDescription" type="system" weight="light" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
<color key="shadowColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</label>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="2fi-mo-0CV" firstAttribute="top" secondItem="KqG-SY-hI1" secondAttribute="bottom" constant="10" id="1Sj-eQ-zIk"/>
<constraint firstItem="3GL-vY-2yd" firstAttribute="centerY" secondItem="kh9-bI-dsS" secondAttribute="centerY" multiplier="0.8" id="X40-O0-ygs"/>
<constraint firstItem="3GL-vY-2yd" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="miu-fV-fUK"/>
<constraint firstItem="KqG-SY-hI1" firstAttribute="centerX" secondItem="kh9-bI-dsS" secondAttribute="centerX" id="pZr-yC-A0n"/>
</constraints>
</view>
</viewController>
Expand Down
35 changes: 34 additions & 1 deletion Example/SwiftGradients/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<UITouch>, with event: UIEvent?) {
let randomChange = arc4random_uniform(4)
switch randomChange {
case 0:
self.gradientLayer.uiColors = [UIColor.random, UIColor.random]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self not needed

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
)
}
}
161 changes: 105 additions & 56 deletions Sources/CAGradientLayerExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add radToDegrees and degreesToRad helpers?

}
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)
}
Comment on lines +107 to 128

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to decide to believe you 😂

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will post the source fro you :)

return isStartPoint ? CGPoint(x: x1, y: y1) : CGPoint(x: x2, y: y2)
}

private class func tanx(_ 𝜽: CGFloat) -> CGFloat {
return tan(𝜽 * CGFloat.pi / 180)
}
}

Expand Down
6 changes: 3 additions & 3 deletions Sources/CALayerGradientsExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public extension CALayer {
@discardableResult
func addGradient(
colors: [UIColor],
angle: Double,
angle: CGFloat,
locations: [Int] = []
) -> CAGradientLayer {
return addGradient(
Expand Down Expand Up @@ -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
}
Expand Down
40 changes: 33 additions & 7 deletions Sources/UIViewGradientsExtension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -35,7 +61,7 @@ public extension UIView {
@discardableResult
func addGradient(
colors: [UIColor],
angle: Double,
angle: CGFloat,
locations: [Int] = []
) -> CAGradientLayer {
return addGradient(
Expand Down Expand Up @@ -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)
}
Expand Down
Loading