CAShapeLayer stroke animated with CADisplayLink not completed - swift

I have set up a CADisplayLink that calls the following drawCircle() function to draw a circle path animation in 10 seconds:
func drawCircle() {
currentDuration = currentDuration + displayLink.duration
circleLayer.strokeEnd = min(CGFloat(currentDuration/maxDuration), 1)
if (currentDuration >= maxDuration) {
stopCircleAnimation()
}
}
func stopCircleAnimation() {
let pausedTime = circleLayer.convertTime(CACurrentMediaTime(), fromLayer: nil)
circleLayer.speed = 0
circleLayer.timeOffset = pausedTime
}
where currentDuration is the elapsed time, and maxDuration is equal to 10. This works fine, except when currentDuration >= maxDuration. Even though the strokeEnd is set to 1, it never fully completes the circle. Why is this??
EDIT
I think it could have something to do with the speed property of the circleLayer. If I set it to a higher amount, e.g. 10, then the circle is completely closed.

This is due to the fact that setting the strokeEnd of your CAShapeLayer generates an implicit animation to the new value. You then set the layer's speed to zero before this animation is complete, therefore 'pausing' the animation, so that it appears 'incomplete'.
While you can work around by disabling implicit animations through setDisableActions – you should probably be considering if using a CADisplayLink is really appropriate here. Core Animation is designed to generate and run animations for you in the first place by generating its own intermediate steps, so why not achieve the same result with an explicit or implicit animation of your layer's strokeEnd?
Here's an example of how you could do this with an implicit animation:
CATransaction.begin()
CATransaction.setAnimationDuration(10)
// if you really want a linear timing function – generally makes the animation look ugly
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear))
circleLayer.strokeEnd = 1
CATransaction.commit()
Or if you want it as an explicit animation:
let anim = CABasicAnimation(keyPath: "strokeEnd")
anim.fromValue = 0
anim.toValue = 1
anim.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
anim.duration = 10
circleLayer.addAnimation(anim, forKey: "strokeEndAnim")
// update model layer value
CATransaction.begin()
CATransaction.setDisableActions(true)
circleLayer.strokeEnd = 1
CATransaction.commit()

Found the answer – disable animations when setting the strokeEnd property:
CATransaction.begin()
CATransaction.setDisableActions(true)
circleLayer.strokeEnd = min(CGFloat(currentDuration/maxDuration), 1)
CATransaction.commit()

Related

How to animate sublayer contents?

I have a layer and want to create an animation for this layer which will update contents of one of sublayers. CAAnimation keyPath has a notation, like sublayers.layerName.propertyName to update some values of a sublayer but seems like it doesn't work with .contents property.
func rightStepAfter(_ t: Double) -> CAAnimation {
let rightStep = CAKeyframeAnimation(keyPath: "sublayers.right.contents")
rightStep.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
rightStep.keyTimes = [0, 1]
rightStep.values = [UIImage(named:"leftfoot")!.cgImage!, UIImage(named:"rightfoot")!.cgImage!]
rightStep.beginTime = t
rightStep.duration = stepDuration
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CAAnimation {
let leftStep = CAKeyframeAnimation(keyPath: "sublayers.left.opacity")
leftStep.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
leftStep.keyTimes = [0, 1]
leftStep.values = [0, 1]
leftStep.beginTime = t
leftStep.duration = stepDuration
leftStep.fillMode = .forwards
return leftStep
}
Here leftStepAfter creates correct animation which updates opacity of a sublayer and rightStepAfter doesn't update contents of a sublayer. If you remove sublayers.right. from the keyPath - animation will correctly change contents of a CURRENT layer. Project to check it and the original project.
Why my animation doesn't work and how to fix it?
Did not find an answer to my original question (via keypath), but solved my problem by creating several animations for each sublayer.

Swift5: Stop CABasicAnimation animation, just when the animation is finished

I have 4 edges, one for each corner, with animation. The only thing that the animation does is to vary the alpha of that border. It goes from 0.05 to 1 that alpha.
I am doing this way to the animation:
private func startAnimation(duration: CFTimeInterval) {
let cornerAnimate = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
cornerAnimate.fromValue = 1
cornerAnimate.toValue = 0.05
cornerAnimate.duration = duration
cornerAnimate.repeatCount = .infinity
cornerAnimate.autoreverses = true
cornerAnimate.isRemovedOnCompletion = false
cornerAnimate.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
corners.forEach { corner in
corner.add(cornerAnimate, forKey: "opacity")
}
}
I have it in .infinity because that's what I want to do. I want the animation to be displayed infinitely, and when I tell it to, at any time, stop it.
But I don't want it to stop abruptly, I want it to stop when the alpha is at 1.0. I mean, when I call the function stopAnimation(), it follows a little bit the animation until it 'finishes that cycle' and when the alpha is at 1.0 then it stops it.
This is what I tried to do, but the animation is still abrupt:
func stopAnimation() {
let endAnimation = CABasicAnimation(keyPath: #keyPath(CALayer.opacity))
var actualOpacity: Double = 0.05
corners.forEach { corner in
actualOpacity = corner.presentation()?.value(forKeyPath: "opacity") as! Double
}
endAnimation.fromValue = actualOpacity
endAnimation.toValue = 1.0
endAnimation.duration = 1.0
corners.forEach { corner in
corner.add(endAnimation, forKey: "end")
corner.removeAnimation(forKey: "opacity")
}
}
It looks like you're building the app in the simulator (since I can see the mouse movement), this appears to be a bug that effects simulators only. I was able to reproduce it in the simulator but not on an actual device.
Run it on a device and you should not be seeing that glitch.

How to make animation in ARkit by changing position per second

I what to make animation in ARkit to a SCNnode by changing its positions, kind of like a render loop. Means to change the location 60 time per second, how can I do that?
If you just want to change your node's position 60 times a second, then you could just implement the renderer 'updateAtTime' function for the 'ARSCNViewDelegate'.
That function should be called at around 60 times a second.
A dumb version might be
extension ViewController: ARSCNViewDelegate, ARSessionDelegate {
func renderer(_ renderer: SCNSceneRenderer, updateAtTime time: TimeInterval) {
yourNode.simdWorldPosition.z -= 0.01
}
That would move the previously defined yourNode 1 cm in the -Z direction 60 times a second. You could write this with an iteration tracker to loop whatever position actions you want.
You could also do an actual animation with something like
let startingZ = node.simdWorldPosition.z
let animation = CABasicAnimation(keyPath: "simdWorldPosition.z")
animation.fromValue = startingZ
animation.toValue = startingZ - 2.0
animation.duration = 1
animation.autoreverses = true
animation.repeatCount = .infinity
yourNode.addAnimation(animation, forKey: "backAndForth")
Or you could use SCNActions as shown here: Adding animation to 3D models in ARKit

How to smoothly finish infinity animation

I have infinity CABasicAnimation which actually simulate pulsating by increasing and decreasing scale:
scaleAnimation.fromValue = 0.5
scaleAnimation.toValue = 1.0
scaleAnimation.duration = 0.8
scaleAnimation.autoreverses = true
scaleAnimation.repeatCount = .greatestFiniteMagnitude
scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
I want to smoothly stop this animation in toValue. In other words, I want to allow current animation cycle finish, but stop repeating. Is there a nice and clean way to do this? I had a few ideas about freezing current animation, removing it and creating a new one with time offset, but maybe there is a better way?
There is a standard way to do this cleanly — though it's actually quite tricky if you don't know about it:
The first thing you do is set the layer's scale to the scale of its presentationLayer.
Then call removeAllAnimations on the layer.
Now do a fast animation where you set the layer's scale to 1.
Here's a possible implementation (for extra credit, I suppose we could adjust the duration of the fast animation to match what the current scale is, but I didn't bother to do that here):
#IBAction func doStop(_ sender: Any) {
let lay = v.layer
lay.transform = lay.presentation()!.transform
lay.removeAllAnimations()
CATransaction.flush()
lay.transform = CATransform3DIdentity
let scaleAnimation = CABasicAnimation(keyPath: "transform")
scaleAnimation.duration = 0.4
scaleAnimation.timingFunction = CAMediaTimingFunction(name: .easeOut)
lay.add(scaleAnimation, forKey: nil)
}
Result:

How to trigger an animation by PERCENTAGE in swift?

Alright, I am trying to trigger an animation incrementally as a scroll view is scrolled. To do this, I have taken this link here and converted it to swift - http://www.oliverfoggin.com/controlling-animations-with-a-uiscrollview/
Giving me the percentage offset x for my scrollview. This is all great.
Problem is I'm fairly new to swift and don't know how to tie this back into my existing animation which is a transform/move instead of changing color.
Here's my animation here-
self.borderlines.transform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
self.activeBorder.layer.opacity = 1
CATransaction.begin()
self.activeBorder.layer.transform = CATransform3DMakeScale(CGFloat(0.03), CGFloat(1.0), 1)
let anim2 = CABasicAnimation(keyPath: "transform")
let fromTransform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
let toTransform = CATransform3DMakeScale(CGFloat(1.0), CGFloat(1.0), 1)
anim2.fromValue = NSValue(CATransform3D: fromTransform)
anim2.toValue = NSValue(CATransform3D: toTransform)
anim2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim2.fillMode = kCAFillModeForwards
anim2.removedOnCompletion = false
self.activeBorder.layer.addAnimation(anim2, forKey: "_activeBorder")
CATransaction.commit()
And then I also have another move animation. As is these are called on touch, so 1 static event.
The guy in the tutorial from what I can see recalculated/triggers his animation EVERY scroll then alters bg color by percentage. I don't know how to apply this to another type of animation- he mentions key frames and I have no idea what those are.
How can I achieve this effect? What do I need to change here?
Look into "freezing" your animation by setting its layer's speed to zero and manipulating its timeOffset as your response to scrolling.