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
}) { }
Related
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.
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.
I need to animate one of [filters, compositionFilter, backFilters] on a CALayer in video composition I tried the following code on NSView layer to test if the code works in the first place
view.wantsLayer = true
view.layerUsesCoreImageFilters = true
if let filter = CIFilter.init(name: "CIGaussianBlur", parameters: ["inputRadius": 50]) {
let animation = CABasicAnimation.init(keyPath: "filters")
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.duration = 2 //for testing
animation.autoreverses = false
animation.fromValue = NSArray.init(array: [])
animation.toValue = NSArray.init(array: [filter])
view.layer.add(animation, forKey: "CIGaussianBlurAnimation")
}
but nothing happens, so I need to know what is the proper way to animate one/all of [filters, compositionFilter, backFilters] properties of CALayer, and whether or not it is supported on AVVideoComposition(on iOS 'I know it is not supported on iOS UIView CALayers')
I found 1/2 the solution
BUT the main question remains: "is there any way to enable CIFilter on CALayer in video composition on iOS"
it is indicated in the documentation, to update any attribute of the filter after is applied to the layer, you should use
layer.setValue(1, forKeyPath: "backgroundFilters.myFilter.inputRadius")
So it means to animate any filter, we should update the filter properties directly in the animation, so the following should be done
for compositingFilter property:
let animation = CABasicAnimation(keyPath: "compositingFilter.inputRadius")
and for a filter in filters property:
filter.name = "filterName"
layer.filters = [someFilters, filter, otherFilters]
let animation = CABasicAnimation(keyPath: "filters.filterName.inputRadius")
and for a filter in backgroundFilters property:
filter.name = "filterName"
layer.backgroundFilters = [someFilters, filter, otherFilters]
let animation = CABasicAnimation(keyPath: "backgroundFilters.filterName.inputRadius")
and set the animation properties, then add it to the layer layer.add(animation, forKey: "animationKey")
NOTE: animationKey could be any value, even a nil value, but reusing the value will remove the previous animation; even when is running
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()
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