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

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.

Related

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:

NSSplitViewItem.isCollapsed ignores animation durations

I am trying to collapse an NSSplitViewItem like so
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.1
context.allowsImplicitAnimation = true
searchItem.isCollapsed = collapsed
}, completionHandler: {
// do stuff
})
No matter what I set for the duration, the animation duration of the collapse animation does not change.
Setting the duration on a CATransaction also does not work.
Checking the header files it mentions this:
The exact animation used can be customized by setting it
in the -animations dictionary with a key of "collapsed".
That raises even more questions. When do I set this animation? What keypath do I animate with this animation? What to/from values does it expect? etc... All I want to do is change its duration.
Solution:
As per #Loengard's answer this is what I went with
NSAnimationContext.runAnimationGroup { _ in
let animation = CABasicAnimation(keyPath: nil)
animation.duration = 0.1
searchItem.animations["collapsed"] = animation
searchItem.animator().isCollapsed = collapsed
}
The dictionary the header file refers to is searchItem.animations. You don't need to specify fromValue or toValue, just customize duration like this:
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.1
context.allowsImplicitAnimation = true
let collapseAnimation = CABasicAnimation(keyPath: "collapsed")
collapseAnimation.duration = 0.1
var existingAnimations = searchItem.animations
existingAnimations["collapsed"] = collapseAnimation
searchItem.animations = existingAnimations
searchItem.isCollapsed = !searchItem.isCollapsed
}) { }

CAShapeLayer stroke animated with CADisplayLink not completed

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()

Cocoa - animating NSWindow

I am trying to animate my NSWindow after I click a specific button, so I wrote a function:
func fadeIn(window: NSWindow, duration: NSTimeInterval) {
var alpha = 0.0 as CGFloat
window.alphaValue = alpha
window.makeKeyAndOrderFront(nil)
let numberOfLoops = Int(duration / 0.02)
let alphaChangeInLoop = (CGFloat(1.0) / CGFloat(numberOfLoops))
var loopRun = 0
for _ in 0...numberOfLoops {
loopRun += 1
alpha += alphaChangeInLoop
window.alphaValue = alpha
NSThread.sleepForTimeInterval(0.020)
}
print("Loop was run \(loopRun) times. Ending alpha: \(alpha)")
if alpha != 1.0 {
alpha = 1.0
window.alphaValue = 1.0
print("Alpha set to 1.0")
}
}
It seems OK for me, but it is not - the window does not fade in. It just appears after whatever I put in the duration field (so it is probably not accepting any alphaValue below 1.0).
Is it possible to animate alphaValue in OSX application? I've been searching this forum and I found some other ways including NSAnimationContext but it did not work for me.
The problem with your approach is that the window typically only redraws when you let flow of execution return to the main event loop. You're not doing that until the end. You could try to force the window to redraw for each iteration of your loop, but don't bother.
Animation is built in to Cocoa. Try this:
window.alphaValue = 0
window.makeKeyAndOrderFront(nil)
NSAnimationContext.runAnimationGroup({ (context) -> Void in
context.duration = duration
window.animator().alphaValue = 1
}, completionHandler: nil)
If you're OK with the default duration of 0.25 seconds, you can just do:
window.alphaValue = 0
window.makeKeyAndOrderFront(nil)
window.animator().alphaValue = 1
You should also be aware that Cocoa applies animations to windows, by default. You should consider just letting it do its thing. If you insist on taking over for it, you should probably do:
window.animationBehavior = .None

Play rhythmic sound when view is spinning

I want to play a rhythmic sound when the view is rotating, dependent on speed. Here I have the spin animation for UIview.
var spinAnimation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation")
spinAnimation.fromValue = 0
spinAnimation.toValue = NSNumber(double: 10.0+Double(arc4random_uniform(360))*M_PI * 3)
spinAnimation.duration = 5
spinAnimation.delegate = self
spinAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
layer.addAnimation(spinAnimation, forKey: "SpinAnimation")
How do I feedback the user about the rotation speed via sound?
In the apple developer document they have given an option to change the volume of the music, pl. refer the the below url
https://developer.apple.com/library/prerelease/ios/documentation/AVFoundation/Reference/AVAudioPlayerClassReference/index.html
and you can use the below code snippet for your reference
var spinnigSpeedMusic= AVAudioPlayer()
var spinningMusicUrl:NSURL = NSBundle.mainBundle().URLForResource("spinningSpeed", withExtension: ".mp33")
spinnigSpeedMusic = AVAudioPlayer(contentsOfURL:spinningMusicUrl, error: nil)
spinnigSpeedMusic.prepareToPlay()
spinnigSpeedMusic.play()
spinnigSpeedMusic.volume = 0.1 // this value can be changed from 0.0 to 1, based on the rotation speed.