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.
Related
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
I have created draw circle animation.
When user tap touchdown on button the animation goes to 100% , but when user move finger away it pauses.
What i want to do now is to make this animation goes to 0% (start value) instead of pause.
var shapeLayer = CAShapeLayer()
var cabAnim = CABasicAnimation()
var firstAnimation = true
override func viewDidLoad() {
super.viewDidLoad()
let button = UIButton(frame: CGRect(x: 100, y: 200, width: 100, height: 50))
button.backgroundColor = .blue
button.setTitle("123", for: .normal)
button.addTarget(self, action: #selector(startCircleAnimation), for: .touchDown)
button.addTarget(self, action: #selector(endCircleAnimation), for: .touchUpInside)
self.view.addSubview(button)
let circlePath = UIBezierPath(arcCenter: CGPoint(x: 0, y: 0), radius: CGFloat(50), startAngle: -CGFloat(Double.pi/2), endAngle: CGFloat(Double.pi * 3/2), clockwise: true)
shapeLayer.path = circlePath.cgPath
shapeLayer.position = CGPoint(x: self.view.frame.midX, y: self.view.frame.midY)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3.0
shapeLayer.strokeEnd = 0.0
self.view.layer.addSublayer(shapeLayer)
}
#objc func startCircleAnimation() {
if (firstAnimation) {
firstAnimation = false
circleAnimation()
} else {
resumeAnimation(layer: shapeLayer)
}
}
#objc func endCircleAnimation() {
pauseAnimation(layer: shapeLayer)
}
func circleAnimation() {
cabAnim = CABasicAnimation(keyPath: "strokeEnd")
cabAnim.duration = 1.5
cabAnim.repeatCount = 1
cabAnim.fromValue = 0.0
cabAnim.toValue = 1.0
let cam = CAMediaTimingFunctionName.linear
cabAnim.timingFunction = CAMediaTimingFunction(name: cam)
shapeLayer.strokeEnd = 1.0
shapeLayer.add(cabAnim, forKey: "animateCircle")
}
func pauseAnimation(layer: CALayer) {
let pausedTime: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil)
layer.speed = 0.0
layer.timeOffset = pausedTime
}
func resumeAnimation(layer: CALayer) {
let resAnimation = CABasicAnimation(keyPath: "strokeEnd")
let pausedTime: CFTimeInterval = layer.timeOffset
layer.speed = 1.0
layer.timeOffset = 0.0
layer.beginTime = 0.0
let timeSincePause: CFTimeInterval = layer.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
layer.beginTime = timeSincePause
}
How can i make value goes to 0 instead of pause?
Edit:
I want it to move from currentValue to 0 (for example i hold button animation goes to 50% and then i remove finger from button and animation goes to 0%)
you can use this function to create the backward animation, it will draw animation from 100 to 0. i have added the whole project you can take a look.
#IBAction func backward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 1
basicAnimation.toValue = 0
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeBackwards
basicAnimation.isRemovedOnCompletion = false
// Callback function
CATransaction.setCompletionBlock {
print("end animation")
}
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
Create ShapeLayer
in most cases, you can call creating shape layer on `ViewDidLoad`
func createShape() {
// let's start by drawing a circle somehow
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)
}
Forward Action
#IBAction func forward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.toValue = 1
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeForwards
basicAnimation.isRemovedOnCompletion = false
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
Backward Action
#IBAction func backward(_ sender: Any) {
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
basicAnimation.fromValue = 1
basicAnimation.toValue = 0
basicAnimation.duration = 2
basicAnimation.fillMode = kCAFillModeBackwards
basicAnimation.isRemovedOnCompletion = false
// Callback function
CATransaction.setCompletionBlock {
print("end animation")
}
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
On hold or resume logics, you can play with these codes.
I am trying to make custom activity indicator, see the indicator class below
import UIKit
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
var count = 0
override func draw(_ rect: CGRect) {
super.draw(rect)
}
func blink() {
backgroundColor = .clear
let duration: CFTimeInterval = 1.2
//let beginTime = CACurrentMediaTime()
let beginTimes: [CFTimeInterval] = [0.25, 1, 1.75, 2.5]
let timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
// Animation
let animation = CAKeyframeAnimation(keyPath: "opacity")
animation.keyTimes = [0, 0.5, 1]
animation.timingFunctions = [timingFunction, timingFunction]
animation.values = [1, 0.3, 1]
animation.duration = duration
animation.repeatCount = HUGE
animation.isRemovedOnCompletion = false
for i in 0...3 {
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.fillColor = UIColor.clear.cgColor
shape.lineWidth = 6.8
shape.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(i) * CGFloat(Double.pi/2) + gap
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap * 2
shape.path = UIBezierPath(arcCenter: center, radius: -20, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
animation.beginTime = beginTimes[i]
shape.add(animation, forKey: "animation")
self.layer.addSublayer(shape)
}
}
func startAnimating() {
blink()
}
}
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
self.view.addSubview(indicator)
indicator.startAnimating()
I have attached my current result.
But you can see that the animation is not in circular motion like standard UIActivityIndicatorView. Can anyone help me to fix this.
Try using a CAReplicatorLayer and instance delay to get everything in sync. Here is a Playground. I am not 100% sure on the visual you want but this should be close.
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
class MyIndicator: UIView {
let gap = CGFloat(.pi/4 / 6.0)
private var replicatorLayer = CAReplicatorLayer()
private var mainShapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonSetup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonSetup()
}
func commonSetup(){
mainShapeLayer = CAShapeLayer()
mainShapeLayer.frame = self.bounds
mainShapeLayer.fillColor = UIColor.clear.cgColor
mainShapeLayer.lineWidth = 6.8
mainShapeLayer.strokeColor = UIColor.blue.cgColor
let startAngle:CGFloat = CGFloat(Double.pi * 2) + gap/2
let endAngle:CGFloat = startAngle + CGFloat(Double.pi/2) - gap/2
mainShapeLayer.path = UIBezierPath(arcCenter: center, radius: self.bounds.midX - 10, startAngle: startAngle, endAngle: endAngle, clockwise: true).cgPath
replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = self.bounds
replicatorLayer.instanceCount = 4
let angle = (Double.pi * 2)/4
replicatorLayer.instanceTransform = CATransform3DRotate(CATransform3DIdentity, CGFloat(angle), 0, 0, 1)
replicatorLayer.addSublayer(mainShapeLayer)
replicatorLayer.opacity = 0
self.layer.addSublayer(replicatorLayer)
}
func animate(){
let defaultDuration : Double = 0.75
let animate = CAKeyframeAnimation(keyPath: "opacity")
animate.values = [1, 0.3, 1]
animate.keyTimes = [0, 0.5, 1]
animate.repeatCount = .greatestFiniteMagnitude
animate.duration = defaultDuration
animate.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
replicatorLayer.instanceDelay = defaultDuration/4
self.mainShapeLayer.add(animate, forKey: nil)
let opacityIn = CABasicAnimation(keyPath: "opacity")
opacityIn.fromValue = 1
opacityIn.toValue = 0
opacityIn.duration = 0.2
replicatorLayer.add(opacityIn, forKey: nil)
self.replicatorLayer.opacity = 1
}
func stopAnimating(){
CATransaction.begin()
let opacityOut = CABasicAnimation(keyPath: "opacity")
opacityOut.fromValue = 1
opacityOut.toValue = 0
opacityOut.duration = 0.2
CATransaction.setCompletionBlock {
[weak self] in
self?.mainShapeLayer.removeAllAnimations()
}
replicatorLayer.add(opacityOut, forKey: nil)
self.replicatorLayer.opacity = 0
CATransaction.commit()
}
override func layoutSubviews() {
super.layoutSubviews()
mainShapeLayer.frame = self.bounds
replicatorLayer.frame = self.bounds
}
}
class MyViewController : UIViewController {
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let indicator = MyIndicator(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
indicator.animate()
//just to simulate starting and stoping
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) {
indicator.stopAnimating()
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 5) {
indicator.animate()
}
}
view.addSubview(indicator)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
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.
I want to create an animation of a circle disappearing and I did the following code (based on other codes which I found on internet).
class CircleView: UIView {
private var circleLayer: CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
self.tintColor = UIColor.lightGrayColor()
self.backgroundColor = UIColor.clearColor()
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
self.drawCircle()
self.addCircleAnimation()
}
func drawCircle() {
let size:CGFloat = CGFloat(100)
self.circleLayer?.removeFromSuperlayer()
self.circleLayer = CAShapeLayer()
self.circleLayer?.frame = self.bounds
let radius = size / 2;
let path = UIBezierPath(arcCenter: CGPointMake(size / 2, size / 2), radius: radius, startAngle: -CGFloat(M_PI) / 4, endAngle: 2 * CGFloat(M_PI) - CGFloat(M_PI) / 4, clockwise: true)
self.circleLayer?.path = path.CGPath
self.circleLayer?.fillColor = UIColor.clearColor().CGColor
self.circleLayer?.strokeColor = self.tintColor.CGColor
self.circleLayer?.lineWidth = CGFloat(2.0)
self.circleLayer?.rasterizationScale = 2.0 * UIScreen.mainScreen().scale
self.circleLayer?.shouldRasterize = true
self.layer.addSublayer(self.circleLayer!)
}
func addCircleAnimation() {
let animation = CABasicAnimation(keyPath: "strokeStart")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = CFTimeInterval(floatLiteral: 1)
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeBackwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.beginTime = CACurrentMediaTime()
animation.delegate = self
self.circleLayer?.addAnimation(animation, forKey:"strokeStart")
}
}
However when the animation finishes it leaves the original circle, even if I detect with animationDidStop it still has an effect of flickering. What's wrong with this code?
Code to remove circle layer:
class CircleView: UIView {
private var circleLayer: CAShapeLayer?
override init(frame: CGRect) {
super.init(frame: frame)
self.commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.commonInit()
}
func commonInit() {
self.tintColor = UIColor.lightGrayColor()
self.backgroundColor = UIColor.clearColor()
}
override func drawRect(rect: CGRect) {
super.drawRect(rect)
self.drawCircle()
self.addCircleAnimation()
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool)
{
self.circleLayer?.removeFromSuperlayer()
}
func drawCircle() {
let size:CGFloat = CGFloat(100)
self.circleLayer?.removeFromSuperlayer()
self.circleLayer = CAShapeLayer()
self.circleLayer?.frame = self.bounds
let radius = size / 2;
let path = UIBezierPath(arcCenter: CGPointMake(size / 2, size / 2), radius: radius, startAngle: -CGFloat(M_PI) / 4, endAngle: 2 * CGFloat(M_PI) - CGFloat(M_PI) / 4, clockwise: true)
self.circleLayer?.path = path.CGPath
self.circleLayer?.fillColor = UIColor.clearColor().CGColor
self.circleLayer?.strokeColor = self.tintColor.CGColor
self.circleLayer?.lineWidth = CGFloat(2.0)
self.circleLayer?.rasterizationScale = 2.0 * UIScreen.mainScreen().scale
self.circleLayer?.shouldRasterize = true
self.layer.addSublayer(self.circleLayer!)
}
/** UPDATED. Add animation for strokEnd instead of strokeStart **/
func addCircleAnimation() {
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.delegate = self
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = CFTimeInterval(floatLiteral: 1)
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeBackwards
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
animation.beginTime = CACurrentMediaTime()
animation.delegate = self
self.circleLayer?.addAnimation(animation, forKey:"strokeEnd")
}
}