Last active
October 26, 2021 18:41
-
-
Save tadija/55e566df601347ebedc449b2aa610c28 to your computer and use it in GitHub Desktop.
AERingView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* https://gist.github.com/tadija/55e566df601347ebedc449b2aa610c28 | |
* Revision 5 | |
* Copyright © 2015-2021 Marko Tadić | |
* Licensed under the MIT license | |
*/ | |
import UIKit | |
@IBDesignable | |
public final class AERingView: UIView { | |
// MARK: Inspectables | |
public var containerColor: CGColor = Defaults.containerColor { | |
didSet { updateContainerUI() } | |
} | |
@IBInspectable | |
public var ringStartDegrees: Double = Defaults.startDegrees { | |
didSet { updateRingPath() } | |
} | |
@IBInspectable | |
public var ringLengthDegrees: Double = Defaults.lengthDegrees { | |
didSet { updateRingPath() } | |
} | |
@IBInspectable | |
public var ringWidth: CGFloat = Defaults.ringWidth { | |
didSet { updateRingUI() } | |
} | |
public var ringStroke: CGColor = Defaults.ringStroke { | |
didSet { updateRingUI() } | |
} | |
public var ringFill: CGColor = Defaults.ringFill { | |
didSet { updateRingUI() } | |
} | |
@IBInspectable | |
public var progressStartDegrees: Double = Defaults.startDegrees { | |
didSet { updateProgressPath() } | |
} | |
@IBInspectable | |
public var progressLengthDegrees: Double = Defaults.lengthDegrees { | |
didSet { updateProgressPath() } | |
} | |
@IBInspectable | |
public var progressWidth: CGFloat = Defaults.progressWidth { | |
didSet { updateProgressUI() } | |
} | |
public var progressStroke: CGColor = Defaults.progressStroke { | |
didSet { updateProgressUI() } | |
} | |
public var progressFill: CGColor = Defaults.progressFill { | |
didSet { updateProgressUI() } | |
} | |
@IBInspectable | |
public var progress: CGFloat = Defaults.progress { | |
didSet { updateProgressCompletion() } | |
} | |
@IBInspectable | |
public var clockwise: Bool = Defaults.clockwise { | |
didSet { updateProgressCompletion() } | |
} | |
@IBInspectable | |
public var reverse: Bool = Defaults.reverse { | |
didSet { updateProgressCompletion() } | |
} | |
// MARK: Properties | |
private(set) public lazy var gradientLayer: CAGradientLayer = { | |
let layer = CAGradientLayer() | |
layer.contentsScale = UIScreen.main.scale | |
layer.drawsAsynchronously = true | |
layer.needsDisplayOnBoundsChange = true | |
self.layer.insertSublayer(layer, at: 0) | |
return layer | |
}() | |
private(set) public lazy var ringLayer: CAShapeLayer = { | |
let layer = CAShapeLayer() | |
self.layer.insertSublayer(layer, at: 1) | |
return layer | |
}() | |
private(set) public lazy var progressLayer: CAShapeLayer = { | |
let layer = CAShapeLayer() | |
self.layer.insertSublayer(layer, at: 2) | |
return layer | |
}() | |
// MARK: API | |
public func resetProgress() { | |
progress = 0.0 | |
} | |
// MARK: Lifecycle | |
override public func layoutSubviews() { | |
super.layoutSubviews() | |
updateUI() | |
} | |
} | |
// MARK: - Extensions | |
private extension AERingView { | |
struct Defaults { | |
static let containerColor = UIColor.clear.cgColor | |
static let startDegrees = 0.0 | |
static let lengthDegrees = 360.0 | |
static let ringWidth = CGFloat(10.0) | |
static let ringStroke = UIColor.lightGray.cgColor | |
static let ringFill = UIColor.clear.cgColor | |
static let progressWidth = CGFloat(10.0) | |
static let progressStroke = UIColor.orange.cgColor | |
static let progressFill = UIColor.clear.cgColor | |
static let progress = CGFloat(0.0) | |
static let clockwise = true | |
static let reverse = false | |
} | |
// MARK: Internal API | |
func updateUI() { | |
updateContainerUI() | |
updateRingPath() | |
updateRingUI() | |
updateProgressPath() | |
updateProgressUI() | |
updateProgressCompletion() | |
} | |
// MARK: Container | |
func updateContainerUI() { | |
backgroundColor = UIColor(cgColor: containerColor) | |
let cornerRadius = bounds.width / 2.0 | |
layer.cornerRadius = cornerRadius | |
gradientLayer.cornerRadius = cornerRadius | |
gradientLayer.frame = bounds | |
} | |
// MARK: Ring | |
func updateRingPath() { | |
ringLayer.frame = layer.bounds | |
let backgroundPadding = (progressWidth - ringWidth) / 2 | |
let inset = ringWidth / 2.0 + backgroundPadding | |
let rect = bounds.insetBy(dx: inset, dy: inset) | |
let path = pathForCircleInRect( | |
rect, | |
startDegrees: ringStartDegrees, | |
lengthDegrees: ringLengthDegrees | |
) | |
ringLayer.path = path | |
} | |
func updateRingUI() { | |
updateShapeLayer( | |
ringLayer, | |
width: ringWidth, | |
fillColor: ringFill, | |
strokeColor: ringStroke | |
) | |
} | |
// MARK: Progress | |
func updateProgressPath() { | |
progressLayer.frame = layer.bounds | |
let inset = progressWidth / 2.0 | |
let rect = bounds.insetBy(dx: inset, dy: inset) | |
let path = pathForCircleInRect( | |
rect, | |
startDegrees: progressStartDegrees, | |
lengthDegrees: progressLengthDegrees | |
) | |
progressLayer.path = path | |
} | |
func updateProgressUI() { | |
updateShapeLayer( | |
progressLayer, | |
width: progressWidth, | |
fillColor: progressFill, | |
strokeColor: progressStroke | |
) | |
} | |
func updateProgressCompletion() { | |
if clockwise { | |
if reverse { | |
progressLayer.strokeStart = progress | |
} else { | |
progressLayer.strokeEnd = progress | |
} | |
} else { | |
if reverse { | |
progressLayer.strokeEnd = 1.0 - progress | |
} else { | |
progressLayer.strokeStart = 1.0 - progress | |
} | |
} | |
} | |
// MARK: Common | |
func pathForCircleInRect(_ rect: CGRect, | |
startDegrees: Double, | |
lengthDegrees: Double) -> CGPath { | |
let center = rect.isEmpty ? | |
CGPoint.zero : CGPoint(x: rect.midX, y: rect.midY) | |
let radius = rect.width / 2.0 | |
let startAngle = startDegrees.degreesToRadians | |
let endAngle = startAngle + lengthDegrees.degreesToRadians | |
let path = UIBezierPath(arcCenter: center, | |
radius: radius, | |
startAngle: startAngle, | |
endAngle: endAngle, | |
clockwise: true) | |
return path.cgPath | |
} | |
func updateShapeLayer(_ layer: CAShapeLayer, | |
width: CGFloat, | |
fillColor: CGColor, | |
strokeColor: CGColor) { | |
layer.lineWidth = width | |
layer.fillColor = fillColor | |
layer.strokeColor = strokeColor | |
} | |
} | |
private extension Double { | |
var degreesToRadians : CGFloat { | |
CGFloat(self * Double.pi / 180.0) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment