CABaseAnimation Delegate - ios

I used CABaseAnimation to create layer's animation,and set CABaseAnimation's delegate to viewController, here is code:
let flyRight = CABasicAnimation(keyPath: "position.x")
flyRight.delegate = self
flyRight.toValue = view.bounds.size.width/2
flyRight.fromValue = -view.bounds.size.width/2
flyRight.fillMode = kCAFillModeBoth
flyRight.duration = 0.5
flyRight.setValue(heading.layer, forKey: "heading")
heading.layer.addAnimation(flyRight, forKey: nil)
flyRight.beginTime = CACurrentMediaTime() + 0.3
flyRight.setValue(username.layer, forKey: "username")
username.layer.addAnimation(flyRight, forKey: nil)
username.layer.position.x = view.bounds.size.width/2
flyRight.beginTime = CACurrentMediaTime() + 0.4
flyRight.setValue(password.layer, forKey: "password")
password.layer.addAnimation(flyRight, forKey: nil)
password.layer.position.x = view.bounds.size.width/2
the delegate animationDidStop method code is as follows:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
print(anim)
if let layer = anim.valueForKey("heading") as? CALayer {
anim.setValue(nil, forKey: "heading")
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 1.5
animation.duration = 1.0
layer.addAnimation(animation, forKey: nil)
print("heading layer animation did stop")
}
}
but in the delegate method I found that the console prints three times "heading layer animation did stop", here is the print line in console:
<CABasicAnimation: 0x7fbe13096b10>
heading layer animation did stop
<CABasicAnimation: 0x7fbe13020bf0>
heading layer animation did stop
<CABasicAnimation: 0x7fbe13096c80>
heading layer animation did stop
I'm very confused as to why the print method executed three times.

Your code says (in essence):
flyRight.delegate = self
flyRight.setValue(heading.layer, forKey: "heading")
heading.layer.addAnimation(flyRight, forKey: nil)
username.layer.addAnimation(flyRight, forKey: nil)
password.layer.addAnimation(flyRight, forKey: nil)
So you are adding this animation to three different layers. Before you did that, you set its "heading" key to heading.layer, and you set the animation's delegate to self. Thus, we have three layers running an animation where delegate is self and where the "heading" key is heading.layer.
So now we come to your animationDidStop handler:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("heading") as? CALayer {
print(/*...*/)
}
}
So all three animations end up calling the animationDidStop handler, and in all three animations the if let layer test passes; all three of them do indeed have a key "heading" which is a CALayer. Hence, all three times you see the printout. It's obvious.
(Indeed, the only thing that is not obvious is why you are surprised. The only explanation I can think of is that you don't realize that addAnimation:forKey: adds a copy of the animation. That is something that some people do not realize. But it is hard to see why not realizing that would cause your surprise in this particular situation.)

Related

UIView animation snaps back to original state before transitioning to a new view

I have some UIButtons that I'm animating indefinitely. The buttons all have 3 sublayers that are added, each of which have their own animation. I'm initializing these animations on viewDidAppear which works great - they fade in and start rotating. The problem is that when I transition to a new view, the animations seem to "snap" back to their initial state, then back to some other state right before the transition occurs. I've tried explicitly removing all of the animations on viewWillDisappear, even tried hiding the entire UIButton itself, but nothing seems to prevent this weird snapping behavior from occurring.
Here's a gif of what's happening (this is me transitioning back and forth between two views):
func animateRotation() {
// Gets called on viewDidAppear
let rotationRight: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationRight.toValue = Double.pi * 2
rotationRight.duration = 4
rotationRight.isCumulative = true
rotationRight.repeatCount = Float.greatestFiniteMagnitude
rotationRight.isRemovedOnCompletion = false
rotationRight.fillMode = .forwards
let rotationLeft: CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotationLeft.toValue = Double.pi * -2
rotationLeft.duration = 3
rotationLeft.isCumulative = true
rotationLeft.repeatCount = Float.greatestFiniteMagnitude
rotationLeft.isRemovedOnCompletion = false
rotationLeft.fillMode = .forwards
let circleImage1 = UIImage(named: "circle_1")?.cgImage
circleLayer1.frame = self.bounds
circleLayer1.contents = circleImage1
circleLayer1.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer1)
let circleImage2 = UIImage(named: "circle_2")?.cgImage
circleLayer2.frame = self.bounds
circleLayer2.contents = circleImage2
circleLayer2.add(rotationLeft, forKey: "rotationAnimation")
layer.addSublayer(circleLayer2)
let circleImage3 = UIImage(named: "circle_3")?.cgImage
circleLayer3.frame = self.bounds
circleLayer3.contents = circleImage3
circleLayer3.add(rotationRight, forKey: "rotationAnimation")
layer.addSublayer(circleLayer3)
}
I would think something as simple as this would hide the button completely as soon as it knows it's going away:
override func viewWillDisappear(_ animated: Bool) {
animatedButton.isHidden = true
}
What's also interesting is that this seems to stop happening if I let it run for a couple minutes. This tells me that it might be some sort of race condition. Just not sure what that race might be...
When you use the UIView set of animations they set the actual state to the final state and then start the animation, ie:
UIView.animate(withDuration: 1) { view.alpha = 0 }
If you check alpha right after the animation starts, it's already 0.
This is a convenience that UIView does for you but that CALayer does not. When you set a CALayer animation you are only setting the animation value, not the actual value of the variable, so when the animation is done, the layer snaps back to its original value. Check the layer value while you are in the middle of the animation and you will see the real value has not changed; only the animated value in the presentation layer has changed.
If you want to replicate the UIView behavior you need to either set the actual final value not he layer when the animation begins or in the use the delegate to set it when the animation ends.

Preserving stokeEnd change from animation without isRemovedOnCompletion set to false

I have a progress circle that animates. I can hit a button and it animates to 20%, if I hit the button again it animates to 40%. I am setting isRemovedOnCompletion to false. However each time I perform an animation a new animation is added to the CAShapelayer. I imagine this is not good for performance. Is there a better way to do this?
Dummy code:
#IBAction func didTapAnimate(_ sender: Any) {
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}
I think no need to remove previous animation as per my knowledge only one animation layer is used for single key so animation automatically replace with previous one.
A string that identifies the animation. Only one animation per unique
key is added to the layer. The special key kCATransition is
automatically used for transition animations. You may specify nil for
this parameter.
So only the last animation occupies the memory previous animation automatically deallocated.
Just remove previous animation from circleLayer by calling removeAnimation(forKey:"yourkey")
#IBAction func didTapAnimate(_ sender: Any) {
circleLayer.removeAnimation(forKey: "MyAnimation")
let animateStroke = CABasicAnimation(keyPath: "strokeEnd")
animateStroke.fromValue = index > 0 ? progressPts[index - 1] : 0
animateStroke.toValue = progressPts[index]
animateStroke.duration = 2.0
animateStroke.fillMode = .forwards
animateStroke.isRemovedOnCompletion = false
circleLayer.add(animateStroke, forKey: "MyAnimation")
index+=1
}

'Press and hold' animation without NSTimer

Update: The NSTimer approach works now, but comes with a huge performance hit. The question is now narrowed down to an approach without NSTimers.
I'm trying to animate a 'Press and hold' interactive animation. After following a load of SO answers, I've mostly followed the approach in Controlling Animation Timing by #david-rönnqvist. And it works, if I use a Slider to pass the layer.timeOffset.
However, I can't seem to find a good way to continuously update the same animation on a press and hold gesture. The animation either doesn't start, only shows the beginning frame or at some points finishes and refuses to start again.
Can anyone help with achieving the following effect, without the horrible NSTimer approach I'm currently experimenting with?
On user press, animation starts, circle fills up.
While user holds (not necessarily moving the finger), the animation should continue until the end and stay on that frame.
When user lifts finger, the animation should reverse, so the circle is empties again.
If the user lifts his finger during the animation or presses down again during the reverse, the animation should respond accordingly and either fill or empty from the current frame.
Here's a Github repo with my current efforts.
As mentioned, the following code works well. It's triggered by a slider and does its job great.
func animationTimeOffsetToPercentage(percentage: Double) {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
let timeOffset = maximumDuration * percentage
print("Set animation to percentage \(percentage) with timeOffset: \(timeOffset)")
fillAnimationLayer.timeOffset = timeOffset
}
However, the following approach with NSTimers works, but has an incredible performance hit. I'm looking for an approach which doesn't use the NSTimer.
func beginAnimation() {
if fillShapeLayer == nil {
fillShapeLayer = constructFillShapeLayer()
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset >= 1.0 {
self.layer.timeOffset = self.maximumDuration
}
else {
self.layer.timeOffset += 0.1
}
})
}
func reverseAnimation() {
guard let fillAnimationLayer = fillShapeLayer, let _ = fillAnimationLayer.animationForKey("animation") else {
print("Animation not found")
return
}
animationTimer?.invalidate()
animationTimer = NSTimer.schedule(interval: 0.1, repeats: true, block: { [unowned self] () -> Void in
if self.layer.timeOffset <= 0.0 {
self.layer.timeOffset = 0.0
}
else {
self.layer.timeOffset -= 0.1
}
})
}
When you use slider you use fillAnimationLayer layer for animation
fillAnimationLayer.timeOffset = timeOffset
However, in beginAnimation and reverseAnimation functions you are using self.layer.
Try to replace self.layer.timeOffset with self.fillShapeLayer!.timeOffset in your timer blocks.
The solution is two-fold;
Make sure the animation doesn't remove itself on completion and keeps its final frame. Easily accomplished with the following lines of code;
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
The hard part; you have to remove the original animation and start a new, fresh reverse animation that begins at the correct point. Doing this, gives me the following code;
func setAnimation(layer: CAShapeLayer, startPath: AnyObject, endPath: AnyObject, duration: Double)
{
// Always create a new animation.
let animation: CABasicAnimation = CABasicAnimation(keyPath: "path")
if let currentAnimation = layer.animationForKey("animation") as? CABasicAnimation {
// If an animation exists, reverse it.
animation.fromValue = currentAnimation.toValue
animation.toValue = currentAnimation.fromValue
let pauseTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
// For the timeSinceStart, we take the minimum from the duration or the time passed.
// If not, holding the animation longer than its duration would cause a delay in the reverse animation.
let timeSinceStart = min(pauseTime - startTime, currentAnimation.duration)
// Now convert for the reverse animation.
let reversePauseTime = currentAnimation.duration - timeSinceStart
animation.beginTime = pauseTime - reversePauseTime
// Remove the old animation
layer.removeAnimationForKey("animation")
// Reset startTime, to be when the reverse WOULD HAVE started.
startTime = animation.beginTime
}
else {
// This happens when there is no current animation happening.
startTime = layer.convertTime(CACurrentMediaTime(), fromLayer: nil)
animation.fromValue = startPath
animation.toValue = endPath
}
animation.duration = duration
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
layer.addAnimation(animation, forKey: "animation")
}
This Apple article explains how to do a proper pause and resume animation, which is converted to use with the reverse animation.

CABasicAnimation Quickly Jumping Before Animation Start

I have a CABasicAnimation animating the strokeEnd property of a CAShapeLayer. Every time I add the animation, it quickly jumps through the animation and then goes and does the real animation (as seen in the image above). If I add the animation in the viewDidLoad, this doesn't happen.
Here's my animation code:
let progressAnim = CABasicAnimation(keyPath: "strokeEnd")
progressAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
progressAnim.duration = 4.5
progressAnim.fromValue = 0.0
progressAnim.toValue = 1.0
progressAnim.removedOnCompletion = false
progressLayer.addAnimation(progressAnim, forKey: "progressAnimation")
progressLayer.strokeEnd = 1.0
I'm not sure what exactly I'm doing wrong, any help would be highly appreciated. Thanks!
My problem was with the line progressLayer.strokeEnd = 1.0. The reason I had that in my code was to stop the animation from going back to it's original values when it finished animating.
The next solution would be to set the fillMode to progressAnim.fillMode = kCAFillModeForwards and removedOnCompletion to progressAnim.removedOnCompletion = false. This sort of fixed my problem. But it created another. In my code, this solution doesn't update the strokeEnd property to the toValue.
My final solution was to set the toValue to the strokeEnd inside override func animationDidStop(anim: CAAnimation, finished flag: Bool).
My animation code:
let progressAnim = CABasicAnimation(keyPath: "strokeEnd")
progressAnim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionDefault)
progressAnim.duration = animationDuration
progressAnim.fromValue = 0.0
progressAnim.toValue = 0.5
progressAnim.delegate = self
progressLayer.addAnimation(progressAnim, forKey: "progressAnimation")
Code for when animation is complete:
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
progressLayer.strokeEnd = 0.5
progressLayer.removeAllAnimations()
}

CABasicAnimation animationDidStart did called, but animationDidStop never was

I saw a lot of answers about this topic, but can not solve my particular case.
Here is a video with complete animation and logging timeline
class AnimationDelegate: UIView {
deinit {
print("AnimationDelegate deinitialized")
}
override func animationDidStart(anim: CAAnimation) {
print("animationDidStart")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
print("animationDidStop")
}
func play() {
let line_1 = CAShapeLayer()
...
let animation = CABasicAnimation()
animation.delegate = self
animation.duration = 3000
animation.repeatCount = 1
animation.keyPath = "strokeEnd"
animation.fromValue = 0
animation.toValue = 360
layer.addSublayer(line_1)
line_1.addAnimation(animation, forKey: "StrokeAnimation")
}
}
let delegate = AnimationDelegate(frame: container.bounds)
container.addSubview(delegate)
delegate.play()
The problem is animationDidStart called, but animationDidStop not.
How this is possible?
The animationDidStop will get called, had you waited longer. But this is, certainly, not the behavior you expected.
I gather that you would like to let the animation run for 3 seconds instead of 3000; and once completed, receive a notification from the delegate callback, which is animationDidStop.
So, instead, you would need to change:
animation.duration = 3
and:
animation.toValue = 1
Thereby, you would see that both animationDidStart and animationDidStop will get called accordingly.
Neither animationDidStart nor animationDidStop worked.
I try to work this lines in a view controller :
let delegate = AnimationDelegate(frame: container.bounds)
container.addSubview(delegate)
delegate.play()
Anitmation worked but override methods didn't work.

Resources