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.