Skip to content

Instantly share code, notes, and snippets.

@tadija
Last active October 26, 2021 18:41
Show Gist options
  • Save tadija/55e566df601347ebedc449b2aa610c28 to your computer and use it in GitHub Desktop.
Save tadija/55e566df601347ebedc449b2aa610c28 to your computer and use it in GitHub Desktop.
AERingView
/**
* 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