CABasicAnimation autoreverse twice as fast - ios

I'm using this code to add pulsing circle with autoreverse:
let scaleAnimation = CABasicAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 6
scaleAnimation.repeatCount = 200
scaleAnimation.autoreverses = true
scaleAnimation.fromValue = 0.1
scaleAnimation.toValue = 0.8
scaleAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.42, 0.0, 0.58, 1.0)
animationView.layer.add(scaleAnimation, forKey: "scale")
What I would like to do here is to:
Run animation fromValue = 0.1 toValue = 0.8 at 2x speed,
and go backwards animating it fromValue = 0.8 toValue = 0.1 at 1x speed.
Is there an easy way to achieve this?

You have two ways of doing this:
CAKeyframeAnimation (best choice for you):
Designed specifically for animating a single keyPath with multiple keyframes, with custom timeFunctions on each interval. Just what you need
let scaleAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
scaleAnimation.duration = 18 // 6 seconds for the first part, 12 for the second
scaleAnimation.repeatCount = 200
scaleAnimation.values = [0.1, 0.8, 0.1] // make sure first and last values are equal in order to get seamless animation
scaleAnimation.keyTimes = [0, 0.333, 1] // keyframes scaled to [0; 1] interval
scaleAnimation.timingFunctions = [
CAMediaTimingFunction(controlPoints: 0.42, 0.0, 0.58, 1.0), //first interval
CAMediaTimingFunction(controlPoints: 0.58, 0.0, 0.42, 1.0) //second interval (reversed)
]
layer.add(scaleAnimation, forKey: nil)
CAAnimationGroup (kind of workaround)
Designed to group animations (perhaps with different keyPaths) for a single layer
let scaleUpAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
//setup first animation as you did
let scaleDownAnimation = CAKeyframeAnimation(keyPath: "transform.scale")
//setup second animation
let groupAnimation = CAAnimationGroup()
groupAnimation.animations = [scaleUpAnimation, scaleDownAnimation]
//setup group if needed
layer.add(groupAnimation, forKey: nil)

Related

Reverse the rotation of UIImageView using CAKeyframeAnimation

I'm using the following code to rotate an image infinitely, the rotation is clockwise but I also need it to reverse rotation to counter clockwise every 1-2 rotations and then back to clockwise, how to get this to work?
/// Image Rotation - CAKeyframeAnimation
func rotate(imageView: UIImageView, rotationSpeed: Double) {
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
animation.duration = rotationSpeed
animation.fillMode = CAMediaTimingFillMode.forwards
animation.repeatCount = .infinity
animation.values = [0, Double.pi/2, Double.pi, Double.pi*3/2, Double.pi*2]
/// Percentage of each key frame
let moments = [NSNumber(value: 0.0), NSNumber(value: 0.1),
NSNumber(value: 0.3), NSNumber(value: 0.8), NSNumber(value: 1.0)]
animation.keyTimes = moments
imageView.layer.add(animation, forKey: "rotate")
}
Set animation.autoreverses = true, will reverse an animation.
for counter clockwise rotation, you can do this:
self.imageView?.transform = CGAffineTransform(rotationAngle: -2*CGFloat.pi)

Multiple Animations For Same UIView in Swift

I am trying to add multiple animations for my image view, but only one of them is animated. Please check the below code. I created scale and rotate animations for my image view but only i see the scale animation when run the below code.
//Rotate animation
let rotation: CABasicAnimation = CABasicAnimation(keyPath:
"transform.rotation.y")
rotation.toValue = 0
rotation.fromValue = 2.61
//Scale animation
let scale: CABasicAnimation = CABasicAnimation(keyPath: "transform.scale")
scale.toValue = 1
scale.fromValue = 0
//Adding animations to group
let group = CAAnimationGroup()
group.animations = [rotation,scale]
group.duration = 0.2
myImage.layer.add(group, forKey: nil)
The rotation occurs but the duration is less to notice
group.duration = 0.2
when changed to 5 seconds see
in your completion you can put your animations, when finish one, wait second.. and etc
let myImage = UIImageView()
UIView.animate(withDuration: 1.0, animations: {
let rotation: CABasicAnimation = CABasicAnimation(keyPath:
"transform.rotation.y")
rotation.toValue = 0
rotation.fromValue = 2.61
}, completion: { (value: Bool) in
UIView.animate(withDuration: 1.0, animations: {
//you can here put your other animation
})
})

CAKeyframeAnimation key times after 1.0 sec not executing

For some strange reason when I put in key times after 1 second they don't appear to execute but when I keep all my key times 1 second and under they all execute properly. Not sure why this is happening, anyone have any ideas? This is the function I'm using:
func animateKeyFrameGroup() {
let opacity = CAKeyframeAnimation(keyPath: "opacity")
opacity.values = [1, 0, 1]
opacity.keyTimes = [0.1, 1.0, 1.5]
let translation = CAKeyframeAnimation(keyPath:"transform.translation")
translation.values = [CGPoint(x: 150, y: 300),CGPoint(x: 100, y: 100),CGPoint(x: 150, y: 300)]
translation.keyTimes = [0.1, 1.0, 1.5]
let cornerRadius = CAKeyframeAnimation(keyPath: "cornerRadius")
cornerRadius.values = [circle.bounds.width, circle.bounds.width/2, circle.bounds.width]
cornerRadius.keyTimes = [0.1, 1.0, 1.5]
let borderColor = CAKeyframeAnimation(keyPath: "borderColor")
borderColor.values = [UIColor.black.cgColor, UIColor.cyan.cgColor, UIColor.black.cgColor]
borderColor.keyTimes = [0.1, 1.0, 1.5]
let keyframeAnimationGroup = CAAnimationGroup()
keyframeAnimationGroup.animations = [translation, cornerRadius, borderColor, opacity]
keyframeAnimationGroup.duration = 2
keyframeAnimationGroup.isRemovedOnCompletion = false
keyframeAnimationGroup.fillMode = kCAFillModeForwards
circle.layer.add(keyframeAnimationGroup, forKey: nil)
}
According to Apple documentation:
Each value in the array is a floating point number between 0.0 and 1.0 that defines the time point (specified as a fraction of the animation’s total duration) at which to apply the corresponding keyframe value. Each successive value in the array must be greater than, or equal to, the previous value. Usually, the number of elements in the array should match the number of elements in the
values
property or the number of control points in the
path
property. If they do not, the timing of your animation might not be what you expect.
So you can change a total animation duration: keyframeAnimationGroup.duration = 2
and keyTimes are the fractions of the total duration. Like: keyTime equal 0.5 -> 2 * 0.5 = 1 sec, keyTime equal 0.75 -> 2 * 0.75 = 1.5 sec, etc.

CAKeyFrameAnimation not Linear for values greater than PI

I am having some trouble to understand why an animation isn't working like expected.
What I am doing is this:
Create a UIBezierPath with an arc to move a Label along this path and animate the paths stroke.
//Start Point is -.pi /2 to let the Arc start at the top.
//self.progress = Value between 0.0 and 1.0
let path : UIBezierPath = UIBezierPath.init(arcCenter: CGPoint.init(x: self.bounds.width * 0.5, y: self.bounds.height * 0.5),
radius: self.bounds.width * 0.5, startAngle: -.pi / 2, endAngle: (2 * self.progress * .pi) - (.pi / 2), clockwise: true)
return path
Add this path to a CAShapeLayer
circlePathLayer.frame = bounds
circlePathLayer.path = self.path.cgPath
circlePathLayer.strokeStart = 0
circlePathLayer.strokeEnd = 1
Animate the strokeEnd property with a CABasicAnimation
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.repeatCount = HUGE
animation.fromValue = 0.0
animation.toValue = 1.0
animation.duration = self.animationDuration
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
Animate the position property of my label with a CAKeyFrameAnimation
let animationScore = CAKeyframeAnimation(keyPath: "position")
//some things I tried to fix
//animationScore.timingFunctions = [CAMediaTimingFunction(controlPoints: 0.250, 0.250, 0.750, 0.750)]
//animationScore.timingFunction = CAMediaTimingFunction.init(name: kCAMediaTimingFunctionLinear)
animationScore.path = self.path.cgPath
animationScore.duration = self.animationDuration
animationScore.isRemovedOnCompletion = false
animationScore.fillMode = kCAFillModeBoth
animationScore.repeatCount = HUGE
Add my animations to layer and label
self.circlePathLayer.add(animation, forKey: nil)
self.scoreLabel.layer.add(animationScore, forKey: nil)
My Problem: For ProgressValues greater than 0.75 my label is not moving in linear speed. Values greater than 0.75 mean that my arc is greater than PI.
For values less than 0.75 my animation works fine and label and strokeend have the same speed and are on top of each other.
GIF :
Please ignore the 100% in the Label in this gif my progress was at a value of 0.76.
You see my Label slows down after three quarters of my circle.
I hope someone can help me.
Many thanks
The keyframe animation introduces an unnecessary complication. Simply rotate the label around the center with the same duration as the shape layer's stroke animation:
(I apologize that my animation starts at the bottom, not the top, but I wasn't looking at your question when I wrote the code and now I'm too lazy to change it!)
So, how is that done? It's three animations, all with the same duration:
The shape layer's strokeEnd, like your animation.
An "arm" running thru the center of the circle, with the label as a sublayer at one end (so that the label appears at the radius of the circle). The arm does a rotation transform animation.
The label does a rotation transform animation in the opposite direction. If it didn't, it would rotate along with its superlayer. (Think of how a Ferris wheel works; your chair is on the end of the arm, but it remains upright with respect to the earth.)
This is the entire animation code:
let anim = CABasicAnimation(keyPath: "transform.rotation.z")
anim.fromValue = 0
anim.toValue = 5
anim.duration = 10
self.arm.layer.add(anim, forKey:nil)
let anim2 = CABasicAnimation(keyPath: "transform.rotation.z")
anim2.fromValue = 0
anim2.toValue = -5
anim2.duration = 10
self.lab.layer.add(anim2, forKey:nil)
let anim3 = CABasicAnimation(keyPath: "strokeEnd")
anim3.fromValue = 0
anim3.toValue = 1
anim3.duration = 10
self.shape.add(anim3, forKey:nil)

CALayer resizing back to original size after transform animation

I'm trying to implement two consecutive transformation animations. When the first animation ends, the second animation is called through the completion handler. Because this is a transformation animation, my issue is that when the first animation finishes, the layer resizes back to the original size, and then the second animation begins. I'd like for the second animation to begin with the new layer size after the first transformation animation. This post Objective-C - CABasicAnimation applying changes after animation? says I have to resize/transform the layer before beginning the first animation, so that when the first animation ends, the layer is actually the new size. I've tried to do that by changing the bounds or actually applying the transform to the layer but its still not working.
override func viewDidAppear(_ animated: Bool) {
buildBar()
}
func buildBar(){
progressBar1.bounds = CGRect(x: 0, y: 0, width: 20, height: 5)
progressBar1.position = CGPoint(x: 0, y: 600)
progressBar1.backgroundColor = UIColor.white.cgColor
view.layer.addSublayer(progressBar1)
extendBar1()
}
func extendBar1(){
CATransaction.begin()
let transform1 = CATransform3DMakeScale(10, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
// self.progressBar1.bounds = CGRect(x: 0, y: 0, width: 200, height: 5)
// self.progressBar1.transform = transform1
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
CATransaction.setCompletionBlock {
self.extendBar2()
}
progressBar1.add(anim, forKey: "transform")
CATransaction.commit()
}
func extendBar2(){
let transform1 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.isRemovedOnCompletion = false
anim.fillMode = kCAFillModeForwards
anim.toValue = NSValue(caTransform3D:transform1)
anim.duration = 5.00
progressBar1.add(anim, forKey: "transform")
}
Because you are modifying the transform property of the layer in both animation, it will be easier here to use CAKeyframeAnimation, which will handle the chaining of the animations for you.
func extendBar(){
let transform1 = CATransform3DMakeScale(10, 1, 1)
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CAKeyframeAnimation()
anim.keyPath = "transform"
anim.values = [progressBar1.transform, transform1, transform2] // the stages of the animation
anim.keyTimes = [0, 0.5, 1] // when they occurs, 0 being the very begining, 1 the end
anim.duration = 10.00
progressBar1.add(anim, forKey: "transform")
progressBar1.transform = transform2 // we set the transform property to the final animation's value
}
A word about the content of values and keyTimes:
We set the first value to be the current transform of progressBar1. This will make sure we start in the current layer's state.
In keyTimes, we say that at the begining, the first value in the values array should be used. We then say at animation's half time, the layer should be transformed in values second value. Hence, the animation between the initial state and the second one will occurs during that time. Then, between 0.5 to 1, we'll go from tranform1 to transform2.
You can read more about animations in this very nice article from objc.io.
If you really need two distinct animations (because maybe you want to add subviews to progressBar1 in between, here's the code that will do it
func extendBar1() {
CATransaction.begin()
CATransaction.setCompletionBlock {
print("side effects")
extendBar2()
}
let transform1 = CATransform3DMakeScale(5, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform1
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform1
CATransaction.commit()
}
func extendBar2() {
CATransaction.begin()
let transform2 = CATransform3DMakeScale(2, 1, 1)
let anim = CABasicAnimation(keyPath: "transform")
anim.fromValue = progressBar1.transform
anim.toValue = transform2
anim.duration = 2.00
progressBar1.add(anim, forKey: "transform")
CATransaction.setDisableActions(true)
progressBar1.transform = transform2
CATransaction.commit()
}
What happens here? Basically, we setup a first "normal animation". So we create the animation, that will modify the presentation layer and set the actual layer to the final first animation's transform.
Then, when the first animation completes, we call extendBar2 which will in turn queue a normal animation.
You also want to call CATransaction.setDisableActions(true) before explicitly updating the transform, otherwise, core animation will create an implicit one, which will override the one created just before.

Resources