When I try to set animation.fillMode = kCAFillModeForwards Xcode can't compile and display an error. "Use of unresolved identifier 'kCAFillModeForwards'".
I've already used this in previous projects without any issues, did somebody already encountered this behaviour?
func animateGradient() {
currentGradient += 1
let animation = CABasicAnimation(keyPath: Animation.keyPath)
animation.duration = animationDuration
animation.toValue = currentGradientSet()
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.delegate = self
gradient.add(animation, forKey: Animation.key)
}
That constant has been removed in favor of a forwards property on the CAMediaTimingFillMode type. As of Swift 4.2 the same thing is written as:
animation.fillMode = .forwards
That said, the combination of a forward fill mode and not removing the animation when it completes is often misused in an attempt to make an animation "stick"/"remain". Unless you are animating the removal of a layer, a cleaner solution is to update the layer to the new value and add an animation—which is removed when it completes—to transition from the previous value.
Related
I have a CABasicAnimation moving a NSHostingView (it's a scrolling marquee text). When the user get his mouse cursor over the view, the animation must stops and it can interact with the view.
I tried the following approaches:
Method 1:
As recommended by Apple documentation (https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreAnimation_guide/AdvancedAnimationTricks/AdvancedAnimationTricks.html#//apple_ref/doc/uid/TP40004514-CH8-SW14), I tried to implement the pause/resume method using the convertTime / speed = 0.
It works, the animation is paused and resumed, but while the animation is paused, the coordinates of the user clic inside the view are the one of the model layer and not the one of the presentation layer. So the click respond to where the user would have click if their was no animation.
Method 2:
I tried the various animation flags on my CABasicAnimation as follow :
let animation = CABasicAnimation(keyPath: "position");
animation.duration = 10
animation.fromValue = [0, self.frame.origin.y]
animation.toValue = [-500, self.frame.origin.y]
animation.isCumulative = true
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
Not matter what I use, when the animation is removed, the view get back to it's original position (it jumps back to it's original position).
Method 3:
I tried to save the current animation value before removing by reading the presentation layer position as follow:
print("before")
let currentPosition = layer.presentation()?.position
print("after")
But this method never returns! (ie. "after" is never printed). Could it be a bug or something?
I'm out of idea now. If anyone has a suggestion, it would help a lot. Thanks!
Accessing layer?.presentation()?.position doesn't work because animation.fromValue and animation.toValue should be CGPoint values:
animation.fromValue = CGPoint(x: 0, y: self.frame.origin.y)
animation.toValue = CGPoint(x: -500, y: self.frame.origin.y)
Then Method 3 works just fine.
This question already has answers here:
CABasicAnimation unlimited repeat without HUGE_VALF?
(2 answers)
Closed 2 years ago.
I am creating my own custom activity indicator. I created a circular view with CAShapeLayer and I managed to stroke the circular layer but I want to do it indefinitely until the user wants to stop. The following is my stroke layer animation code.
private func getStrokeEndAnimation()->CABasicAnimation{
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 2.0
animation.fillMode = .forwards
animation.isRemovedOnCompletion = false
return animation
}
There is an instance property on BasicAnimation named repeatCount but if I specify that my activity indicator will animate the given number of times like if I do
animation.repeatCount = 3
it will animate only 3 times. How can I make sure that the animation keeps going indefinitely until, I stop it.
animation.repeatCount = .greatestFiniteMagnitude will for all practical purposes repeat it forever.
You can use infinity in repeat count according to Apple docs
Infinity compares greater than all finite numbers and equal to other
infinite values.
animation.repeatCount = .infinity
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 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:
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()