CABasicAnimation gets completed when showing another vc modally - ios

I use the following way to pause/resume animation
func pauseAnimation(){
var pausedTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
func resumeAnimation(){
var pausedTime = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), fromLayer: nil) - pausedTime
layer.beginTime = timeSincePause
}
It works like a charm as long as the ViewController is currently presented.
When I present modally another view controller and then dismiss it, animation is done, no matter how much time elapsed during this present/dismiss action.
Do you have any suggestions why this might happen? How to fix it? I'd like to add that all other views holds their state, only the animation is completed.
EDIT:
I just figured out that it happens regardles of pausing/resuming - ongoing animation gets completed as well in such scenario.
Here is my code that shows animation implementation
import Foundation
import UIKit
class CircleView: UIView {
var circleLayer: CAShapeLayer!
#IBOutlet var view: UIView!
#IBOutlet weak var progressLabel: UILabel!
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
Bundle.main.loadNibNamed("CircleView", owner: self, options: nil)
self.addSubview(view)
view.frame = self.bounds
}
override func awakeFromNib() {
super.awakeFromNib()
setupAppearance()
}
func setupAppearance() {
progressLabel.textColor = UIColor.textColor
progressLabel.font = UIFont.textTimerClock
}
func setup(progress:Double, clockwise:Bool) {
self.backgroundColor = UIColor.clear
var strokeColor = UIColor.positiveProgressColor
if !clockwise { strokeColor = UIColor.positiveProgressColor }
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(.pi * 2 * progress), clockwise: clockwise)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = strokeColor.cgColor
circleLayer.lineWidth = 8.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
//add grey path
let greyCircleLayer = CAShapeLayer()
let greyCirclePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(.pi * 2.0), clockwise: true)
greyCircleLayer.path = greyCirclePath.cgPath
greyCircleLayer.fillColor = UIColor.clear.cgColor
greyCircleLayer.strokeColor = UIColor.appLightGrey.cgColor
greyCircleLayer.lineWidth = 1.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.insertSublayer(greyCircleLayer, below: circleLayer)
if progressLabel != nil {
progressLabel.text = "10"}
}
func pauseAnimation(){
let pausedTime = circleLayer.convertTime(CACurrentMediaTime(), from: nil)
circleLayer.speed = 0.0
circleLayer.timeOffset = pausedTime
}
func resumeAnimation(){
let pausedTime = circleLayer.timeOffset
circleLayer.speed = 1.0
circleLayer.timeOffset = 0.0
circleLayer.beginTime = 0.0
let timeSincePause = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
circleLayer.beginTime = timeSincePause
}
func animateCircle(duration: TimeInterval, color: UIColor) {
// We want to animate the strokeEnd property of the circleLayer
circleLayer.strokeColor = color.cgColor
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAAnimationLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
Another thing worth mentioning: init(), awakeFromNib() are not being called again so this is not the case.
Another: The same happens for pushing VC instead of presenting modally.

In general when you display another view controller, the view of current view controller is removed from the window. This will also remove all pending animations from their layers, and any existing animation completion handler is called with false, because the animation is not completed (see also https://stackoverflow.com/a/21200504/2352344).
To continue the animation after returning to the view controller, you should reconstruct the animation object in viewWillAppear.

Related

why isn't the CAShapeLayer showing up sometimes?

I am using the following custom UIView class as a loading indicator in my app. Most of the time it works great and I can just uses loadingView.startLoading() to make it appear. However, in rare cases, only the UIView appears with a shadow behind it, and the CAShapeLayer is nowhere to be seen. What could be preventing the CAShapeLayer from appearing on top of the LoadingView UIView?
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
self.layer.addSublayer(circleLayer)
calculateCirclePath()
animateCircle(duration: 1, repeats: true)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
private func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
}
func stopLoading() {
loadingView.isHidden = true
}
}
Because a sublayer and animation can't happen in one go, I changed the code to what is shown below and now it works perfectly!
LoadingView
class LoadingView: UIView {
//MARK: - Properties and Init
let circleLayer = CAShapeLayer()
private var circlePath : UIBezierPath = .init()
let size : CGFloat = 80
init() {
super.init(frame: CGRect(x: 0, y: 0, width: size, height: size))
tag = 100
addShadow(shadowColor: UIColor.label.cgColor, shadowOffset: CGSize(width: 0, height: 0), shadowOpacity: 0.3, shadowRadius: 3)
backgroundColor = .secondarySystemBackground
layer.cornerRadius = size/8
self.layer.addSublayer(circleLayer)
calculateCirclePath()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.blue.cgColor
circleLayer.lineWidth = 5.0
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
self.layer.addSublayer(circleLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
//MARK: - Private UI Setup Methods
private func calculateCirclePath() {
self.circlePath = UIBezierPath(arcCenter: CGPoint(x: size / 2, y: size / 2), radius: size / 3, startAngle: 0.0, endAngle: CGFloat(Double.pi*2), clockwise: true)
}
override func layoutSubviews() {
circleLayer.frame = self.layer.bounds
}
func animateCircle(duration: TimeInterval, repeats: Bool) {
// Setup the CAShapeLayer with the path, colors, and line width
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.duration = duration
animation.repeatCount = repeats ? .infinity : 1
animation.fromValue = 0
animation.toValue = 1
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
circleLayer.strokeEnd = 0
circleLayer.add(animation, forKey: "animateCircle")
let rotationAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationAnimation.repeatCount = repeats ? .infinity : 1
rotationAnimation.fromValue = 0.0
rotationAnimation.toValue = Double.pi*3
rotationAnimation.duration = duration
rotationAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(rotationAnimation, forKey: nil)
let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.repeatCount = repeats ? .infinity : 1
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = duration
fadeAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
circleLayer.add(fadeAnimation, forKey: nil)
}
}
Extension for ViewController
extension ViewController {
func startLoading() {
view.addSubview(loadingView)
loadingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
loadingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
loadingView.widthAnchor.constraint(equalToConstant: CGFloat(80)),
loadingView.heightAnchor.constraint(equalToConstant: CGFloat(80)),
])
loadingView.isHidden = false
loadingView.animateCircle(duration: 1, repeats: true)
}
func stopLoading() {
loadingView.isHidden = true
}
}
its better practice to call this method in main thread using dispatchQueue as sometime control may be in background thread so it must get returned to main thread before performing any UI updates

How to prevent CABasicAnimation from scale all content inside UIView?

I've wrote this to apply heart beat animation to CAShapeLayer, after this worked fine, I need to implement it to be behind UIButton (btnTest) without scaling the UIButton or any other content.
#IBOutlet weak var btnTest: UIButton!
let btnLayer = CAShapeLayer()
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
btnTest.layer.removeAllAnimations()
let ovalPath = UIBezierPath(arcCenter: CGPoint(x: btnTest.frame.midX, y: btnTest.frame.midY), radius: 100, startAngle: 0*(CGFloat.pi / 180), endAngle: 360*(CGFloat.pi / 180), clockwise: true)
btnLayer.path = ovalPath.cgPath
btnLayer.fillColor = UIColor.red.cgColor
btnLayer.contentsGravity = "center"
btnLayer.opacity = 0.3
self.view.layer.addSublayer(btnLayer)
let theAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
theAnimation.duration = 0.75
theAnimation.repeatCount = Float.infinity
theAnimation.autoreverses = true
theAnimation.fromValue = 1.0
theAnimation.toValue = 1.2
theAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
self.view.layer.add(theAnimation, forKey: nil)
}
the is this result gif
another solution was changing this line :
self.view.layer.add(theAnimation, forKey: nil)
to this :
btnLayer.add(theAnimation, forKey: nil)
the result of this was this :gif
Any ideas to solve this problem !
You want to animate your btnLayer. The reason it's animating from the wrong place is probably that the layer's anchorPoint is at 0,0, where it should be set to 0.5, 0.5.
Edit:
Actually, I think the issue is where you put your btnLayer. You should make it a sublayer of your button view's layer, and give it a frame that's the bounds of the button's layer:
btnTest.layer.addSublayer(btnLayer)
btnLayer.frame = btnTest.layer.bounds
You can do it like this, just translate your layer alongside with scale animation. To do so you need to create a CAAnimationGroup.
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
btnTest.layer.removeAllAnimations()
let ovalPath = UIBezierPath(arcCenter: CGPoint(x: btnTest.frame.midX, y: btnTest.frame.midY), radius: 100, startAngle: 0*(CGFloat.pi / 180), endAngle: 360*(CGFloat.pi / 180), clockwise: true)
btnLayer.path = ovalPath.cgPath
btnLayer.fillColor = UIColor.red.cgColor
btnLayer.anchorPoint = CGPoint.init(x: 0.5, y: 0.5)
btnLayer.contentsGravity = "center"
btnLayer.opacity = 0.3
self.view.layer.addSublayer(btnLayer)
let theAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
theAnimation.fromValue = 1.0
theAnimation.toValue = 1.2
let theAnimationTranslationX = CABasicAnimation(keyPath: "transform.translation.x")
theAnimationTranslationX.fromValue = btnTest.bounds.minX
theAnimationTranslationX.toValue = btnTest.bounds.minX - 40
let theAnimationTranslationY = CABasicAnimation(keyPath: "transform.translation.y")
theAnimationTranslationY.fromValue = btnTest.bounds.minY
theAnimationTranslationY.toValue = btnTest.bounds.minY - 80
let animationGroup = CAAnimationGroup.init()
animationGroup.duration = 0.75
animationGroup.repeatCount = Float.infinity
animationGroup.autoreverses = true
animationGroup.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
animationGroup.animations = [theAnimation,theAnimationTranslationX,theAnimationTranslationY]
btnLayer.add(animationGroup, forKey: nil)
}
1- First make the heart shape that will animate a UIView let's name it heartView.
2- self.view.addsubview(heartView) (self.view can be changed to any parent you want)
3- make an invisible button called heartButton and give it the same frame of heartView
4- self.view.addsubview(heartButton) (self.view can be changed to any parent you want)
5- start the heartView animation.
The key point here is not to add the button on the animating view but on their parent (self.view this case) since if the button was on top of the heart and the heart is animating, the button will adopt the animation.
Hope this helps!

add and remove a subview using a button

Hey guys so I've drawn a circle and added it as a subview using a button. When I try to remove it using the removefromsuperview it didn't go away. Please check the code below.
I added a button named Removecircle so I can add or remove the circle but that didn't go as planned.
import UIKit
import GLKit
class ViewController: UIViewController {
var numb = 0
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func Removecircle(_ sender: Any) {
let radius = Int( view.frame.maxX )
let HvalueX = 0
let HvalueY = Int( view.frame.maxY )/2 - Int( view.frame.maxX )/2
// Create a new CircleView
let circleView = CircleView( frame:CGRect(x: HvalueX, y: HvalueY, width: radius, height: radius ))
//let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight))
if numb%2 == 0 {
view.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(duration: 4.0)
circleView.circleLayer.strokeColor = UIColor.blue.cgColor
}else if numb%2 == 1 {
circleView.removeFromSuperview()
}
numb = numb + 1
}
#IBOutlet weak var removecircle: UIButton!
}
class CircleView: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.white.cgColor
circleLayer.lineWidth = 3.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
animateCircle(duration: 2)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
}
You’re creating a new CircleView every time you press the button. Instead, create it the first time and store it as a property of ViewController. Then when you press the button you should be able to remove it with removeFromSuperview().

Argument passed to call that takes no arguments

I am adding in a animated circle to a uiview. In this line of code:
var circleView = addCircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
I am getting an error that reads "Argument passed to call that takes no arguments" pointing to CGRectMake.
I have aisles attached the rest of the code incase it is needed
import UIKit
import CoreMotion
import CoreGraphics
class Animation: UIView {
var circleLayer: CAShapeLayer!
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = UIColor.clear
// Use UIBezierPath as an easy way to create the CGPath for the layer.
// The path should be the entire circle.
let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true)
// Setup the CAShapeLayer with the path, colors, and line width
circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.red.cgColor
circleLayer.lineWidth = 5.0;
// Don't draw the circle initially
circleLayer.strokeEnd = 0.0
// Add the circleLayer to the view's layer's sublayers
layer.addSublayer(circleLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateCircle(duration: TimeInterval) {
// We want to animate the strokeEnd property of the circleLayer
let animation = CABasicAnimation(keyPath: "strokeEnd")
// Set the animation duration appropriately
animation.duration = duration
// Animate from 0 (no circle) to 1 (full circle)
animation.fromValue = 0
animation.toValue = 1
// Do a linear animation (i.e. the speed of the animation stays the same)
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
// Set the circleLayer's strokeEnd property to 1.0 now so that it's the
// right value when the animation ends.
circleLayer.strokeEnd = 1.0
// Do the actual animation
circleLayer.add(animation, forKey: "animateCircle")
}
func addCircleView() {
let diceRoll = CGFloat(Int(arc4random_uniform(7))*50)
var circleWidth = CGFloat(200)
var circleHeight = circleWidth
// Create a new CircleView
var circleView = addCircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight))
UIView.addSubview(circleView)
// Animate the drawing of the circle over the course of 1 second
circleView.animateCircle(1.0)
}
}
Credits to Mike S
While I'd change all references of CGRectMake to CGRect, the issue is with the call to addCircle(). You didn't define any arguments.
Try changing things to:
func addCircleView(frame: CGRect) {
Or, since addCircleView looks like it doesn't use this parameter, try removing the CGRect/CGRectMake from the call to addCircle():
var circleView = addCircleView()
(It looks like you probably want the former.)

Swift 3 animation issue

I am implementing progress view that simply animate colors.
Everything is working fine exept "strokeEnd" animation that is filling by shape and making it to flicker.
Video resource
Code example:
class MaterialCircularProgressView: UIView {
let circularLayer = CAShapeLayer()
let googleColors = [
UIColor(red:0.282, green:0.522, blue:0.929, alpha:1),
UIColor(red:0.859, green:0.196, blue:0.212, alpha:1),
UIColor(red:0.957, green:0.761, blue:0.051, alpha:1),
UIColor(red:0.235, green:0.729, blue:0.329, alpha:1)
]
let inAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
return animation
}()
let outAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "strokeStart")
animation.beginTime = 0.5
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
return animation
}()
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0.0
animation.toValue = 2 * M_PI
animation.duration = 2.0
animation.repeatCount = MAXFLOAT
return animation
}()
var colorIndex : Int = 0
override init(frame: CGRect) {
super.init(frame: frame)
circularLayer.lineWidth = 4.0
circularLayer.fillColor = nil
layer.addSublayer(circularLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
let center = CGPoint(x: bounds.midX, y: bounds.midY)
let radius = min(bounds.width, bounds.height) / 2 - circularLayer.lineWidth / 2
let arcPath = UIBezierPath(arcCenter: CGPoint.zero, radius: radius, startAngle: CGFloat(M_PI_2), endAngle: CGFloat(M_PI_2 + (2 * M_PI)), clockwise: true)
circularLayer.position = center
circularLayer.path = arcPath.cgPath
animateProgressView()
circularLayer.add(rotationAnimation, forKey: "rotateAnimation")
}
func animateProgressView() {
circularLayer.removeAnimation(forKey: "strokeAnimation")
circularLayer.strokeColor = googleColors[colorIndex].cgColor
let strokeAnimationGroup = CAAnimationGroup()
strokeAnimationGroup.duration = 1.0 + outAnimation.beginTime
strokeAnimationGroup.repeatCount = 1
strokeAnimationGroup.animations = [inAnimation, outAnimation]
strokeAnimationGroup.delegate = self
circularLayer.add(strokeAnimationGroup, forKey: "strokeAnimation")
colorIndex += 1;
colorIndex = colorIndex % googleColors.count;
}
}
extension MaterialCircularProgressView: CAAnimationDelegate{
override func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
if(flag) {
animateProgressView()
}
}
}
You can fix that flickering by setting strokeStart to the final value of your outAnimation in the initializer:
circularLayer.strokeStart = 1.0
The reason is that actual value of property animated by CAPropertyAnimation is not automatically set to the toValue of animation, so it reverts to it's original value which is 0.0 in your case. That's why a full circle appears for some time.
However there is a delay before next circle appears, not sure if that was your intention or another bug to be fixed.

Resources