Swift: consecutive CABasicAnimations that repeat - ios

Goal:
First animation runs for a duration of 1.0
At 1.0, the second animation runs (while the first animation autoreverses back to its starting point)
At 2.0, the first animation is back at its starting point, the second animation has completed, and the first animation repeats
At 3.0, the second animation runs as the first animation concludes its second run
My code:
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.fromValue = 1.0
animation.toValue = 1.5
animation.duration = 1.0
animation.autoreverses = true
animation.repeatCount = .greatestFiniteMagnitude
image1.layer.add(animation, forKey: animation.keyPath)
let animation2 = CABasicAnimation(keyPath: "transform.scale")
animation2.fromValue = 1.0
animation2.toValue = 2.0
animation2.duration = 1.0
animation2.fillMode = .forwards
let animation2b = CABasicAnimation(keyPath: "opacity")
animation2b.fromValue = 1.0
animation2b.toValue = 0.0
animation2b.duration = 1.0
animation2b.fillMode = .forwards
let animationGroup = CAAnimationGroup()
animationGroup.animations = [animation2, animation2b]
animationGroup.duration = 2.0
animationGroup.beginTime = 1.0
animationGroup.repeatCount = .greatestFiniteMagnitude
image2.layer.add(animationGroup, forKey: "scaleAndFade")
The goal is to start the 2nd animation 1.0 after the 1st animation. And since the animation group has a duration of 2.0 while the animations within it have a duration of only 1.0, the animation would start at 1.0, end at 2.0, and then not repeat again until 3.0
The two animations sometimes match up, but not on every build. Is there a more surefire way of starting the second animation to begin exactly at the end of the initial animation's first completed animation? So that they'll be in sync from that point on. Thanks for any help!

I’m a little unclear on what we’re trying to achieve, but my sense is that the idea is that we have two views with repeating animations that need to be coordinated. To demonstrate an example of how this might be done, I chose to make both animations be a scale-up followed by a scale-down:
The animated gif here comes to an end after a few repetitions, but in actual fact the repetition just goes on forever.
That was simply achieved as follows:
func step1() {
UIView.animate(withDuration: 1, animations: {
self.v1.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
self.v2.transform = .identity
}) { _ in
DispatchQueue.main.async {
self.step2()
}
}
}
func step2() {
UIView.animate(withDuration: 1, animations: {
self.v1.transform = .identity
self.v2.transform = CGAffineTransform(scaleX: 1.5, y: 1.5)
}) { _ in
DispatchQueue.main.async {
self.step1()
}
}
}
The point is that the stages of the animation can never get out of sync between the two views, because each stage involves both views and follows the previous stage. So I’m pretty sure you can adapt that approach to fit your animation.

I like #matt's answer and always appreciate their input but since I was trying to use CAAnimation (specifically I wanted to use CAKeyframeAnimation), I ended up nesting two CATransactions using CATransaction.begin() and CATransaction.setCompletionBlock to start one animation exactly at the end of the other and then to recursively call the function repeatedly.
CATransaction documentation

Related

How do i keep on rotating an image until a response from a server in swift

I am making an async call whenever a button is clicked now I want an image which is like a refresh icon to be rotating or spinning until I get a response from my server. What i have now only rotates pi that's 180 and when the response arrives too the image doesn't reset. Have pasted my sample code below:
//When the update button is clicked
#objc func updateHome(){
UIView.animate(withDuration: 1) {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
}
getBalances()
}
//Inside getBalances function
DispatchQueue.main.async {
if responseCode == 0 {
let today = self.getTodayString()
print("Time: \(today)")
UIView.animate(withDuration: 1) {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
}
}
This code rotates your UIImageView indefinitely using CABasicAnimation:
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(.pi * 2.0)
rotateAnimation.duration = 1.0 // Change this to change how many seconds a rotation takes
rotateAnimation.repeatCount = Float.greatestFiniteMagnitude
updateIcon.layer.add(rotateAnimation, forKey: "rotate")
And when you get your signal from your server you can call
updateIcon.layer.removeAnimation(forKey: "rotate")
to stop the animation
For continuous rotation, you can use CAKeyframeAnimation too like below.
#objc func updateHome() {
let animation = CAKeyframeAnimation(keyPath: "transform.rotation.z")
animation.duration = 1.0
animation.fillMode = kCAFillModeForwards
animation.repeatCount = .infinity
animation.values = [0, Double.pi/2, Double.pi, Double.pi*3/2, Double.pi*2]
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
self.updateIcon.layer.add(animation, forKey: "rotate")
getBalances()
}
In your Async method (inside)DispatchQueue.main.async, you can stop like below. This will keep you to the original position of the image.
self.updateIcon.layer.removeAllAnimations()
One way you can handle this is to add an instance variable that tracks when the work is completed:
var finished: Bool = false
Then write a function that rotates the image and uses the completion handler to call itself to continue rotating:
func rotateImage() {
UIView.animate(withDuration: 1,
delay: 0.0,
options: .curveLinear
animations: {
self.updateIcon.transform = CGAffineTransform(rotationAngle: .pi)
},
completion: { completed in
if !self.finished {
self.rotateImage()
}
}
}
}
If you want to stop the animation instantly, you should set finished = true and call updateIcon.layer.removeAllAnimations()

how to animation speed slow when animation stop

My code is below. How can I speed slow when animation has stop?
extension UIView{
func rotate() {
let rotation : CABasicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
rotation.fromValue = 0.0
rotation.toValue = 25
rotation.duration = 1.5
rotation.isCumulative = true
rotation.repeatCount = 1
self.layer.add(rotation, forKey: "rotationAnimation")
}
}
Please find following details and add below line in your code,
rotation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
EaseInOut An ease-in ease-out curve causes the animation to begin
slowly, accelerate through the middle of its duration, and then slow
again before completing. This is the default curve for most
animations.
EaseIn An ease-in curve causes the animation to begin slowly, and then
speed up as it progresses.
EaseOut An ease-out curve causes the animation to begin quickly, and
then slow down as it completes.
Hope this helps to you and let me know in case of any queries.
You can use self.layer.speed to decrease animation speed smoothly
You can do it like this
Note: You need to do some modification here it is not tested in xcode
let timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { (timer) in
self.layer.timeOffset = self.layer.convertTime(CACurrentMediaTime(), from: nil)
if self.layer.speed == 0 { timer.invalidate() }
self.layer.beginTime = CACurrentMediaTime()
self.layer.speed -= 0.5
}
timer.fire()
Hope it is helpful

How to make image spin (animation)

I have an image which is a square and I know how to make it spin. But not sure how to make it spin exactly like this animation here:
Notice how it spins... then stops a little... then spins again... and so on.
What I have just is a basic spin but doesn't look like the gif above:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 3) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2)
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount=Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
Any thoughts on how this is done?
You need to use a timing function that accelerates and decelerates, also known as an ease-in-ease-out timing function.
I've modified your function to use Core Animation's standard ease-in-ease-out timing function:
extension UIView {
func rotate360Degrees(duration: CFTimeInterval = 0.8) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let radians = CGFloat.pi / 4
rotateAnimation.fromValue = radians
rotateAnimation.toValue = radians + .pi
rotateAnimation.isRemovedOnCompletion = false
rotateAnimation.duration = duration
rotateAnimation.repeatCount=Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
}
Result:
Note that in your image, it looks like the box pauses every 180 degrees, so I've changed the function to rotate by only 180 degrees. Since the box has 90 degree radial symmetry, it still looks like it goes all the way around, with pauses at 180 and 360 degrees.
If you need to animate an image without any radial symmetry, you'll need to use a CAKeyframeAnimation to achieve the ease-in-ease-out at both 180 and 360 degrees.
Similar effect can also be achieved with a repeated animation with a delay.
UIView.animate(withDuration: 1,
delay: 0.2,
options: [.repeat],
animations: { imageView.transform = imageView.transform.rotated(by: CGFloat.pi) },
completion: nil)

How do eliminate the "jump" in my CABasicAnimation?

Using this code:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = Int(0)
let multiplicand = multiplier * 2
rotation!.toValue = Int(Double(multiplicand) * .pi)
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
}
I get the animation I want. (Either a wheel spins, or a cell rotates fast enough in the opposite direction to always remain exactly right side up).
However, when the 30 seconds (duration) is up, there is a flicker as the view jumps back to how it looked before the animation.
I understand it is supposed to work this way.
How do I apply the rotation to the "before" image so that when the duration expires I don't see any cells jump?
Increasing the duration of the animation slows the wheel's spin, so that is not an appropriate solution.
If #22521690 applies, I don't understand how - I do not have an explicit CATransaction.
Try
rotation!.toValue = Double(multiplicand) * .pi
instead of
rotation!.toValue = Int(Double(multiplicand) * .pi)
The issue is with the radian precision which is lost due to Int conversion.
On the next line after applying the animation to the layer, set the property you're animating to its ending value.
Because an animation is "in-flight", the normal display of the layer is covered the presentation layer, a special animation layer.
Once the animation is complete, the presentation layer is hidden/removed, and the actual layer is exposed. If it is a the same state as the presentation layer at the end of the animation then there's no jump.
That code might look like this:
func rotateTheView(_ aView: UIView, inClockwiseDirection isClockwise: Bool) {
let multiplier = (isClockwise ? 1 : -1)
let key = (isClockwise ? "Spin" : "Rotate")
var rotation: CABasicAnimation?
rotation = CABasicAnimation(keyPath: "transform.rotation")
rotation!.fromValue = 0.0
let multiplicand = multiplier * 2
let finalAngle = Double(multiplicand) * .pi
rotation!.toValue = finalAngle
rotation!.duration = 30 // Speed
rotation!.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotation!.repeatCount = HUGE //HUGE_VALF Repeat forever.
aView.layer.add(rotation!, forKey: key)
//-------------------
//Set the final transform on the layer to the final rotation,
//but without animation
CATransaction.begin()
CATransaction.setDisableActions(true)
let finalTransform = CATransform3DMakeRotation(CGFloat(finalAngle), 0.0, 0.0, 1.0)
aView.layer.transform = finalTransform
CATransaction.commit()
//-------------------
}

Flipping x animation with repetition after a certain duration

I was trying to do a "flip x animation" for a UIButton. I have seen some documentation on how to do this animation but i couldn't find how to do it with repetition. For example, i want to flip the image of the UIButton and change it after a time interval of 5 sec with repetition.
Any help? Thanks.
You can use CAAnimationGroup to group an array of animations. Total animation for whole animationGroup is 6.0, flip animation takes place for 1.0 second. so remaining 5.0 seconds act as delay here.
EDIT: CATransaction.setCompletionBlock can be used but it will only call completion after all animations are completed. So now the flip will occur only for one time and on completion you will receive completion call, and update image view and initiate flip animation again.
Like this
imageView.flip {
// update imageView and call it again.
}
extension UIView {
func flip(completion: #escaping ()->()) {
CATransaction.begin()
CATransaction.setCompletionBlock {
// completion code here
completion()
}
let animationGroup = CAAnimationGroup()
animationGroup.duration = 6.0
animationGroup.repeatCount = 1
let easeInOut = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let animation = CABasicAnimation(keyPath: "transform")
animation.duration = 1.0
animation.autoreverses = true
animation.fromValue = CATransform3DIdentity
animation.toValue = NSValue(caTransform3D: CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0))
animation.timingFunction = easeInOut
animationGroup.animations = [animation]
layer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
layer.add(animationGroup, forKey: "flip")
CATransaction.commit()
}
}

Resources