CAKeyframeAnimation delay before repeating - iphone

I have a CAKeyframeAnimation animation that I would like to repeat forever using repeatCount = HUGE_VALF. The animation's duration is 2 seconds, but I would like to have a 3 seconds pause before each cycle.
The only 2 ways I can think of doing that are:
Make the whole animation last 5 seconds and add extra keyTimes and values so that I get the pause I'm looking for during the last 3s of the 5s animation. This feels kinda hacky.
Have the animation only repeat once and then add use something like performSelector:afterDelay:2 to run the animation again, and so on and so on. This feels dirty as well. Also would mean that I need to call addAnimation: every 5 seconds, which I'm not sure is optimal in terms of performance.
Is there another option I might be missing? Is one of those 2 methods better than the other?

By dumping the animations of Apple's MKUserLocationView, I was able to see how they were doing it. Turns out that this is what CAAnimationGroup is for. By encapsulating a 2 seconds animation into a 5 seconds animation group, you'll end up with a 2 seconds animation followed by a 3 seconds delay:
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.duration = 5;
animationGroup.repeatCount = INFINITY;
CAMediaTimingFunction *easeOut = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
CABasicAnimation *pulseAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.xy"];
pulseAnimation.fromValue = #0.0;
pulseAnimation.toValue = #1.0;
pulseAnimation.duration = 2;
pulseAnimation.timingFunction = easeOut;
animationGroup.animations = #[pulseAnimation];
[ringImageView.layer addAnimation:animationGroup forKey:#"pulse"];

samvermette's answer in Swift 3:
let animationGroup = CAAnimationGroup()
animationGroup.duration = 5;
animationGroup.repeatCount = .infinity
let easeOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
let pulseAnimation = CABasicAnimation(keyPath: "transform.scale.xy")
pulseAnimation.fromValue = 0
pulseAnimation.toValue = 1.0
pulseAnimation.duration = 2
pulseAnimation.timingFunction = easeOut
animationGroup.animations = [pulseAnimation]
ringImageView.layer.add(animationGroup, forKey: "pulse")

Tested on Swift 4.2
Just implement the following extension and then you can get a nice pulse Animation on Any UI component
//MARK:- Pulse animation
extension UIView {
func applyPulse(_ apply: Bool = true) {
if apply {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.toValue = 1.5
animation.duration = 0.8
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeOut)
animation.autoreverses = true
animation.repeatCount = Float.infinity
self.layer.add(animation, forKey: "pulsing")
} else {
self.layer.removeAnimation(forKey: "pulsing")
}
}
}
Usage:
To start
yourUiComponent.applyPulse()
To Stop
yourUiComponent.applyPulse(false)

Related

Change Color of an Animated UIBezierPath at a Certain Point

I am trying to create a circular progress bar in Swift 4 using a CAShapeLayer and animated UIBezierPath. This works fine but I would like the circle to change it's strokeColor once the animation reaches a certain value.
For example: Once the circle is 75% drawn I want to switch the strokeColor from UIColor.black.cgColor to UIColor.red.cgColor.
My code for the circle and the "progress" animation looks like this:
let circleLayer = CAShapeLayer()
// set initial strokeColor:
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.path = UIBezierPath([...]).cgPath
// animate the circle:
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
animation.fromValue = 0.0
animation.toValue = 1
animation.duration = 10
animation.isAdditive = true
animation.fillMode = .forwards
circleLayer.add(animation, forKey: "strokeEnd")
I know that is also possible to create a CABasicAnimation for the strokeColor keypath and set the fromValue and toValue to UIColors to get the strokeColor to slowly change. But this is like a transition over time which is not exactly what I want.
Update 1:
Based on Mihai Fratu's answer I was able to solve my problem. For future reference I want to add a minimal Swift 4 code example:
// Create the layer with the circle path (UIBezierPath)
let circlePathLayer = CAShapeLayer()
circlePathLayer.path = UIBezierPath([...]).cgPath
circlePathLayer.strokeEnd = 0.0
circlePathLayer.strokeColor = UIColor.black.cgColor
circlePathLayer.fillColor = UIColor.clear.cgColor
self.layer.addSublayer(circlePathLayer)
// Create animation to animate the progress (circle slowly draws)
let progressAnimation = CABasicAnimation()
progressAnimation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
progressAnimation.fromValue = 0.0
progressAnimation.toValue = 1
// Create animation to change the color
let colorAnimation = CABasicAnimation()
colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
colorAnimation.fromValue = UIColor.black.cgColor
colorAnimation.toValue = UIColor.red.cgColor
colorAnimation.beginTime = 3.75 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
colorAnimation.duration = 0.001 // make this really small - this way you "hide" the transition
colorAnimation.fillMode = .forwards
// Group animations together
let progressAndColorAnimation = CAAnimationGroup()
progressAndColorAnimation.animations = [progressAnimation, colorAnimation]
progressAndColorAnimation.duration = 5
// Add animations to the layer
circlePathLayer.add(progressAndColorAnimation, forKey: "strokeEndAndColor")
If I understood your question right this should do what you are after. Please bare in mind that it's not tested at all:
let circleLayer = CAShapeLayer()
// set initial strokeColor:
circleLayer.strokeColor = UIColor.black.cgColor
circleLayer.path = UIBezierPath([...]).cgPath
// animate the circle:
let animation = CABasicAnimation()
animation.keyPath = #keyPath(CAShapeLayer.strokeEnd)
animation.fromValue = 0.0
animation.toValue = 1
animation.beginTime = 0 // Being part of an animation group this is relative to the animation group start time
animation.duration = 10
animation.isAdditive = true
animation.fillMode = .forwards
// animate the circle color:
let colorAnimation = CABasicAnimation()
colorAnimation.keyPath = #keyPath(CAShapeLayer.strokeColor)
colorAnimation.fromValue = UIColor.black.cgColor
colorAnimation.toValue = UIColor.black.red
colorAnimation.beginTime = 7.5 // Since your total animation is 10s long, 75% is 7.5s - play with this if you need something else
colorAnimation.duration = 0.0001 // make this really small - this way you "hide" the transition
colorAnimation.isAdditive = true
colorAnimation.fillMode = .forwards
let sizeAndColorAnimation = CAAnimationGroup()
sizeAndColorAnimation.animations = [animation, colorAnimation]
sizeAndColorAnimation.duration = 10
circleLayer.add(sizeAndColorAnimation, forKey: "strokeEndAndColor")

Animate roundedrect with CoreAnimation

Guys I have the following code to produce a bezierpath
firstLine.path = UIBezierPath(roundedRect: CGRect(x: frameSize.width / 2.2, y: frameSize.height / 2.6, width: 4, height: 4), cornerRadius: 10).cgPath
I try to animate the firstLine by moving it to the right of the screen by x value and back to the original position. But I don't know what to use for the keyPath in the animation.
let animation = CABasicAnimation(keyPath: "??")
animation.toValue = // this should be the position to move?
animation.duration = 0.5 // duration of the animation
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut) // animation curve is Ease Out
animation.fillMode = kCAFillModeBoth // keep to value after finishing
animation.isRemovedOnCompletion = false // don't remove after finishing
animation.autoreverses = true
animation.repeatCount = HUGE
Can someone help me out on this :)
From: https://developer.apple.com/reference/quartzcore/cabasicanimation
You create an instance of CABasicAnimation using the inherited init(keyPath:) method, specifying the key path of the property to be animated in the render tree.
Example of what you want:
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = [startX, startY]
animation.toValue = [destinationX, destinationY]

How to make a video pause when a calayer appears

I am trying to make a calayer pause the video when it appears. I do not know how to go about doing this whether I add it as another calayer or animation. Here is my code when I add the calayer before exporting it. So when the animation happens I would like to have the video pause when it appears and then resume once the animation stops.
let titleLayer = CATextLayer()
titleLayer.backgroundColor = NSColor.clearColor().CGColor
titleLayer.string = "Dummy text"
titleLayer.font = NSFont(name: "Helvetica", size: 28)
titleLayer.shadowOpacity = 0.5
titleLayer.alignmentMode = kCAAlignmentCenter
titleLayer.frame = CGRectMake(0, 50, size.width, size.height / 6)
let animation: CABasicAnimation = CABasicAnimation(keyPath: "opacity")
animation.duration = 0
animation.fromValue = Int(1.0)
animation.toValue = Int(0.0)
animation.beginTime = 5
animation.removedOnCompletion = false
animation.fillMode = kCAFillModeForwards
titleLayer.addAnimation(animation, forKey: "animateOpacity")
You can pause the video before you add your animation to the layer and then resume it when the animation finishes. To know when the animation finishes, you can have an object be a delegate of animation and then resume playback in the animationDidStop:finished: delegate method.
func showTitle() {
// configure animation { .. }
animation.delegate = self
// pause video
titleLayer.addAnimation(animation, forKey: "animateOpacity")
}
func animationDidStop(anim: CAAnimation, finished flag: Bool) {
// play video
}

How to animate the whole screen when colision?

I want to make the screen shake when an enemy colide with the player. I have been searching for a posible answer but I don find anything. If someone can help me, thanks.
try this
func shakeFrame(scene: SKScene) {
let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x - 4.0, scene.view!.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x + 4.0, scene.view!.center.y))
scene.view!.layer.addAnimation(animation, forKey: "position")
}
in your case try this
func shakeFrame(scene: SKScene) {
let animation: CABasicAnimation = CABasicAnimation(keyPath: "position")
animation.duration = 0.05
animation.repeatCount = 4
animation.autoreverses = true
animation.fromValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x - 4.0, scene.view!.center.y))
animation.toValue = NSValue(CGPoint: CGPointMake(scene.view!.center.x + 4.0, scene.view!.center.y))
scene.view!.layer.addAnimation(animation, forKey: "position")
}
if hits < 2 && circuloPrincipal.color != enemigo.color {
shakeFrame()
}

How do I animate opacity using swift?

Could someone please provide me with an example of animating the opacity of an Image View in swift??
I couldn't even find a good example in objective c
func showCorrectImage(element: AnyObject){
var animation : CABasicAnimation = CABasicAnimation(keyPath: "opacity");
animation.delegate = self
animation.fromValue = NSValue(nonretainedObject: 0.0)
animation.toValue = NSValue(nonretainedObject: 1.0)
animation.duration = 1.0
element.layer?.addAnimation(animation, forKey: nil)
}
I think I have most of this right (not completely sure though), could someone please help me?
element = an Image View
Thanks in advance!~
If you need a simple animation, why not just use UIView's animateWithDuration:animations: method?
imageView.alpha = 0
UIView.animateWithDuration(1.0) {
imageView.alpha = 1
}
You can also write it like this:
animation.fromValue = 0.0
animation.toValue = 1.0
The whole code should be like this:
let animation = CABasicAnimation(#keyPath(CALayer.opacity))
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
element.layer?.addAnimation(animation, forKey: "fade")
You can use this extension method (unless you want to modify the view's attributes itself):
extension CALayer {
func flash(duration: TimeInterval) -> Observable<Void> {
let flash = CABasicAnimation(keyPath: "opacity")
flash.fromValue = NSNumber(value: 0)
flash.toValue = NSNumber(value: 1)
flash.duration = duration
flash.autoreverses = true
removeAnimation(forKey: "flashAnimation")
add(flash, forKey: "flashAnimation")
opacity = 0 // Change the actual data value in the layer to the final value
}
}
If anyone is looking to use CABasicAnimation like OP had originally tried to do, one problem with his original code was he was using NSValue for the toValue. I switched it to NSNumber and it worked for me. Like this
fadeToVisible.fromValue = NSNumber(float: 0.0)
Thanks to #ylin0x81. For swift 5 i had to do:
cell.imageView.alpha = 0.0;
UIView.animate(withDuration: 1.0) {
cell.imageView.alpha = 1
}