After a fair amount of trial and error, I have gotten the animation acting in the manner that I want it, as seen here. However, I always feel like I'm writing way too much and too sloppily when trying to use Core Animation.
The animation is as follows:
- The view begins with a point of light at the top of the loop.
- When the user initiates the timer, the glowing line takes the prescribed amount of time to fill the loop.
- When complete, the loop stays lit until acknowledged by the user.
- When acknowledged, the tail of the glowing line chases the head and returns to being a single point at the top of the screen.
- Repeat
The code controlling the animation is all wrapped in a subclass of UIView
:
class LoopView: UIView {
// ...
lazy var path: UIBezierPath = initializedPath()
lazy var animationLayer: CAShapeLayer = initializedAnimationLayer()
override func draw(_ rect: CGRect) {
layer.addSublayer(animationLayer)
// ...
}
// ...
}
Here are the path and animation layer initializations:
private func intializedPath() -> UIBezierPath {
return UIBezierPath(ovalIn: bounds)
}
private func initializedAnimationLayer() -> CAShapeLayer {
let layer = CAShapeLayer()
let transform = CGAffineTransform(rotationAngle: 1.5 * CGFloat.pi).translatedBy(x: -bounds.width, y: 0)
layer.path = path.cgPath
layer.setAffineTransform(transform)
layer.strokeColor = timerColor.cgColor
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = kCALineCapRound
layer.lineWidth = lineWidth
layer.shadowColor = timerColor.cgColor
layer.shadowRadius = 5.0
layer.shadowOpacity = 1
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.strokeEnd = CGFloat.leastNormalMagnitude
return layer
}
And finally, the meat of the animation:
func animateTimer(over duration: TimeInterval) {
let head = CABasicAnimation(keyPath: "strokeEnd")
head.fromValue = CGFloat.leastNormalMagnitude
head.toValue = 1
head.duration = duration
head.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
head.fillMode = kCAFillModeForwards
head.isRemovedOnCompletion = false
animationLayer.add(head, forKey: "strokeEnd")
}
func animateTimerReset() {
UIView.animate(withDuration: 0, animations: { self.animationLayer.strokeStart = 1 }) { (_) in
self.animationLayer = self.initializedAnimationLayer()
self.layer.addSublayer(self.animationLayer)
}
}
Where I spent the most time on figuring it out was that I didn't want to discard the animationLayer
every time I wanted to reset the animation. I also had to use different methods of defining the animation for the fill and the collapse, which feels wrong, but trying to use the same strategies wasn't getting the results I wanted.
For reference, the full class code is in this gist
1 Answer 1
There is one problem with your approach: Additional sublayers are added
in draw()
and in animateTimerReset()
and never removed. That can
cause memory problems and slower drawing eventually.
I would suggest to create and add only a single instance of the animation layer, this can for example be done in
override func awakeFromNib() {
layer.addSublayer(animationLayer)
}
Then animate this single animation layer:
func animateTimer(over duration: TimeInterval) {
animationLayer.removeAllAnimations()
let head = CABasicAnimation(keyPath: "strokeEnd")
head.toValue = 1
head.duration = duration
head.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
head.fillMode = kCAFillModeForwards
head.isRemovedOnCompletion = false
animationLayer.add(head, forKey: "strokeEnd")
}
func animateTimerReset() {
let tail = CABasicAnimation(keyPath: "strokeStart")
tail.toValue = 1
tail.duration = 0.1
tail.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
tail.fillMode = kCAFillModeForwards
tail.isRemovedOnCompletion = false
animationLayer.add(tail, forKey: "strokeStart")
}
Some further remarks: At several places the explicit type annotation can be removed, e.g.
var trackColor: UIColor = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1)
var trackShade: UIColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
var trackLight: UIColor = UIColor(red: 1, green: 1, blue: 1, alpha: 0.3)
is shorter written as
var trackColor = UIColor(red: 0.3, green: 0.3, blue: 0.3, alpha: 1)
var trackShade = UIColor(red: 0, green: 0, blue: 0, alpha: 0.6)
var trackLight = UIColor(red: 1, green: 1, blue: 1, alpha: 0.3)
Also "implicit member expressions" can be used if the type is inferred from the context, e.g.
layer.strokeEnd = CGFloat.leastNormalMagnitude
becomes
layer.strokeEnd = .leastNormalMagnitude