I have implemented a circular loading animation exactly as in this video:
https://www.youtube.com/watch?v=O3ltwjDJaMk
Here is my code:
class ViewController: UIViewController {
let shapeLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
let center = view.center
let trackLayer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
trackLayer.path = circularPath.cgPath
trackLayer.strokeColor = UIColor.lightGray.cgColor
trackLayer.lineWidth = 10
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = kCALineCapRound
view.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 0
view.layer.addSublayer(shapeLayer)
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
}
@objc private func handleTap() {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
}
When the animation happens I want the color of the circle to go from red towards green, if the circle is fully completed it should be green, if not then some color between red-green (depending on variable from the user-id). How can this be done?
You can use CAAnimation
to animate color as well. So you can follow that link.
The part that is missing for you is to compute the from
and to
colors which need to be interpolated based on current values.
Assuming that your progress view will at some point have updated interface to have
var minimumValue: CGFloat = 0.0
var maximumValue: CGFloat = 100.0
var currentValue: CGFloat = 30.0 // 30%
then with those values you can compute a current progress, a value between 0
and 1
which should define color interpolation scale. A basic math to compute it should be:
let progress = (currentValue-maximumValue)/(minimumValue-maximumValue) // TODO: handle division by zero. Handle current value out of bounds.
With progress you can now interpolate any numeric value by using
func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
return values.from + (values.to - values.from)*scale
}
but a color is not a numeric value. In case of CAAnimation
you should break it down to 4 numeric values as RGBA for consistency (at least I believe CAAnimation
uses interpolation for color in RGBA space). Just as a note; in many cases it looks nicer when you interpolate in HSV space rather than RGB.
So interpolating a color should look something like this:
func rgba(_ color: UIColor) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
return values.from + (values.to - values.from)*scale
}
func interpolate(_ values: (from: UIColor, to: UIColor), scale: CGFloat) -> UIColor {
let startColorComponents = rgba(values.from)
let endColorComponents = rgba(values.to)
return .init(red: interpolate((startColorComponents.r, endColorComponents.r), scale: scale),
green: interpolate((startColorComponents.g, endColorComponents.g), scale: scale),
blue: interpolate((startColorComponents.b, endColorComponents.b), scale: scale),
alpha: interpolate((startColorComponents.a, endColorComponents.a), scale: scale))
}
These should be all components that you need for your animation and I hope it will be enough to put you on the right track. I personally like to have more control plus I like to avoid tools such as CAAnimation
and even shape layers. So this is how I would accomplish your task:
class ViewController: UIViewController {
@IBOutlet private var progressView: ProgressView?
override func viewDidLoad() {
super.viewDidLoad()
view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
}
@objc private func onTap() {
guard let progressView = progressView else { return }
progressView.animateValue(to: .random(in: progressView.minimumValue...progressView.maximumValue), duration: 0.3)
}
}
@IBDesignable class ProgressView: UIView {
@IBInspectable var minimumValue: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
@IBInspectable var maximumValue: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
@IBInspectable var value: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
@IBInspectable var colorAtZeroProgress: UIColor = .black { didSet { setNeedsDisplay() } }
@IBInspectable var colorAtFullProgress: UIColor = .black { didSet { setNeedsDisplay() } }
@IBInspectable var lineWidth: CGFloat = 10.0 { didSet { setNeedsDisplay() } }
private var currentTimer: Timer?
func animateValue(to: CGFloat, duration: TimeInterval) {
currentTimer?.invalidate()
let startTime = Date()
let startValue = self.value
currentTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { timer in
let progress = CGFloat(max(0.0, min(Date().timeIntervalSince(startTime)/duration, 1.0)))
if progress >= 1.0 {
// End of animation
timer.invalidate()
}
self.value = startValue + (to - startValue)*progress
})
}
override func draw(_ rect: CGRect) {
super.draw(rect)
let progress = max(0.0, min((value-minimumValue)/(maximumValue-minimumValue), 1.0))
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height)*0.5 - lineWidth*0.5
let startAngle: CGFloat = -.pi*2.0
let endAngle: CGFloat = startAngle + .pi*2.0*progress
let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
let interpolatedColor: UIColor = {
func rgba(_ color: UIColor) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
var r: CGFloat = 0.0
var g: CGFloat = 0.0
var b: CGFloat = 0.0
var a: CGFloat = 0.0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
return (r, g, b, a)
}
func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
return values.from + (values.to - values.from)*scale
}
func interpolate(_ values: (from: UIColor, to: UIColor), scale: CGFloat) -> UIColor {
let startColorComponents = rgba(values.from)
let endColorComponents = rgba(values.to)
return .init(red: interpolate((startColorComponents.r, endColorComponents.r), scale: scale),
green: interpolate((startColorComponents.g, endColorComponents.g), scale: scale),
blue: interpolate((startColorComponents.b, endColorComponents.b), scale: scale),
alpha: interpolate((startColorComponents.a, endColorComponents.a), scale: scale))
}
return interpolate((self.colorAtZeroProgress, self.colorAtFullProgress), scale: progress)
}()
interpolatedColor.setStroke()
circularPath.lineCapStyle = .round
circularPath.lineWidth = lineWidth
circularPath.stroke()
}
}
Feel free to use it and modify it anyway you please.