I tried to create an activity graphs, like the activity graph on Apple watch. (3 rings with animation)
here is my full code:
private(set) var animationTime: [CFTimeInterval] = [1, 1, 1]
private(set) var values: [Double] = [0, 0, 0]
private(set) var colors: [UIColor] = [UIColor.red, CUIColor.blue, CUIColor.green]
private(set) var radiusRatios: [Double] = [1, 1, 1]
// to add new value
func setCircle(number index: Int, data: Double) {
values[index] = data
}
var drawing = false
var animationOption = UIView.AnimationOptions()
override public func draw(_ rect: CGRect) {
if !drawing {
switch style {
case .ring:
drawing = true
let borderThickness = (rect.width / 20).rounded().nextUp
let circleDistance = (rect.width / 20).rounded().nextUp
self.layer.sublayers?.removeAll()
var maximumAnimationDuration: TimeInterval = 0
for index in 0 ..< dataSize {
let belowCircle = CAShapeLayer()
let mainCircle = CAShapeLayer()
layer.addSublayer(belowCircle)
layer.addSublayer(mainCircle)
belowCircle.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
mainCircle.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
let outerCircularPath = UIBezierPath(arcCenter: .zero,
radius: rect.width / 2
- borderThickness * CGFloat(2 * index + 1) / 2
- circleDistance * CGFloat(index),
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
belowCircle.path = outerCircularPath.cgPath
mainCircle.path = outerCircularPath.cgPath
belowCircle.opacity = 0.2
belowCircle.strokeColor = colors[index].cgColor
belowCircle.lineWidth = borderThickness
belowCircle.fillColor = UIColor.clear.cgColor
belowCircle.lineCap = CAShapeLayerLineCap.round
mainCircle.strokeColor = colors[index].cgColor
mainCircle.lineWidth = borderThickness
mainCircle.fillColor = UIColor.clear.cgColor
mainCircle.lineCap = CAShapeLayerLineCap.round
mainCircle.strokeEnd = 0
mainCircle.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)
// outer circle
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.toValue = values[index] < 0.001 ? 0.001 : values[index]
animation.duration = animationTime[index]
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
mainCircle.add(animation, forKey: "RingChartOuterCircle\(index)")
maximumAnimationDuration = max(maximumAnimationDuration, animationTime[index])
}
UIView.transition(with: self,
duration: maximumAnimationDuration,
options: animationOption,
animations: nil,
completion: { [weak self] _ in
self?.drawing = false
})
And when I want to use it, I use this function from the parent view controller:
public func setChartsValus(outerValue: Double, middleValue: Double, innerValue: Double) {
ringChart.setNeedsDisplay()
ringChart.setCircle(number: 0, data: outerValue)
ringChart.setCircle(number: 1, data: middleValue)
ringChart.setCircle(number: 2, data: innerValue)
}
like that:
setChartsValus(outerValue: 0.5, middleValue: 0.8, innerValue: 0.6)
My problem is, when I want to add a new value (it will happen every few minutes), I have to remove all sub layers and doing the process again, that is why, every time it's called, the all rings back to zero and move from 0 again to fill the value, which is not good at all. this problem happen because of :
self.layer.sublayers?.removeAll()
If I don't remove all, all layers come on each other and it become so mess.
I want, when I add a new value from example 0.7, and the old value is 0.3, the ring start to grow from 0.3 to reach 0.7 (like apple watch activity app), not back to zero and fill it again.
I would really be appreciated if everyone can help me on this? I spent two days to fix it but I couldn't find any way.
Thank you so much
My solution is to remove the previous layers, add new layers with the same strokeEnd and set it animate to new values
override public func draw(_ rect: CGRect) {
transform(rect)
}
func transform(_ rect: CGRect, to newValues: [CGFloat]? = nil) {
if !drawing {
switch style {
case .ring:
drawing = true
let borderThickness = (rect.width / 20).rounded().nextUp
let circleDistance = (rect.width / 20).rounded().nextUp
self.layer.sublayers?.removeAll()
for index in 0 ..< 3 {
let belowCircle = CAShapeLayer()
let mainCircle = CAShapeLayer()
layer.addSublayer(belowCircle)
layer.addSublayer(mainCircle)
belowCircle.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
mainCircle.position = CGPoint(x: layer.bounds.midX, y: layer.bounds.midY)
let outerCircularPath = UIBezierPath(arcCenter: .zero,
radius: rect.width / 2
- borderThickness * CGFloat(2 * index + 1) / 2
- circleDistance * CGFloat(index),
startAngle: 0,
endAngle: 2 * CGFloat.pi,
clockwise: true)
belowCircle.path = outerCircularPath.cgPath
mainCircle.path = outerCircularPath.cgPath
belowCircle.opacity = 0.2
belowCircle.strokeColor = colors[index].cgColor
belowCircle.lineWidth = borderThickness
belowCircle.fillColor = UIColor.clear.cgColor
belowCircle.lineCap = CAShapeLayerLineCap.round
mainCircle.strokeColor = colors[index].cgColor
mainCircle.lineWidth = borderThickness
mainCircle.fillColor = UIColor.clear.cgColor
mainCircle.lineCap = CAShapeLayerLineCap.round
if let newValues = newValues {
mainCircle.strokeEnd = values[index]
} else {
mainCircle.strokeEnd = 0
}
mainCircle.transform = CATransform3DMakeRotation(-CGFloat.pi / 2, 0, 0, 1)
let animation = CABasicAnimation(keyPath: "strokeEnd")
if let newValues = newValues {
animation.toValue = newValues[index] < 0.001 ? 0.001 : newValues[index]
} else {
animation.toValue = values[index] < 0.001 ? 0.001 : values[index]
}
animation.duration = animationTime[index]
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
mainCircle.add(animation, forKey: "RingChartOuterCircle\(index)")
}
drawing = false
case .chart:
break
}
}
In container view controller, I call function transform again with my new values:
override func viewDidLoad() {
super.viewDidLoad()
setChartsValus(outerValue: outerValue, middleValue: middleValue, innerValue: innerValue)
// suppose we need to change 10% every 2 second
Timer.scheduledTimer(timeInterval: 2, target: self, selector: #selector(changeValue), userInfo: nil, repeats: true)
}
@objc func changeValue() {
outerValue += 0.1
innerValue += 0.1
middleValue += 0.1
ringChart.layer.sublayers?.removeAll()
ringChart.transform(ringChart.bounds, to: [outerValue, middleValue, innerValue])
}