CAAnimationGroup timing - swift

This is strange to me
For the following code (where myView is a coloured UIView on the storyboard) the timing seems off.
class ViewController: UIViewController {
#IBOutlet weak var myView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let round = CABasicAnimation(keyPath: "cornerRadius")
round.fromValue = 0.0
round.toValue = 50.0
round.duration = 2.0
round.beginTime = 0.0
round.fillMode = CAMediaTimingFillMode.backwards
round.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
myView.layer.cornerRadius = 50.0
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 1.0
scaleDown.toValue = 0.25
scaleDown.beginTime = 2.0
scaleDown.duration = 2.0
let rotate = CABasicAnimation(keyPath: "transform.rotation")
rotate.fromValue = .pi/10.0
rotate.toValue = 0.0
rotate.beginTime = 4.0
rotate.duration = 2.0
let hideAnimation = CABasicAnimation(keyPath: "hidden")
hideAnimation.fromValue = NSNumber(value: 1)
hideAnimation.toValue = NSNumber(value: 0)
hideAnimation.beginTime = 4.0
hideAnimation.duration = 2.0
let imageGroupAnimations = CAAnimationGroup()
imageGroupAnimations.animations = [round, scaleDown, rotate, hideAnimation]
imageGroupAnimations.duration = 6.0
imageGroupAnimations.repeatCount = 1
imageGroupAnimations.beginTime = 0.0
myView.layer.add(imageGroupAnimations, forKey: nil)
}
}
The duration of the animation group is 6.0, so I would expect the shape to be hidden (since I expect it to be hidden from 4 to 4 + 2 (6.0) seconds.
However, the shape appears and finishes off the animation (finish rotating). I've tried using CACurrentMediaTime() in front of every beginTime - but this also doesn't have the expected result.
Expected result:
0-2 seconds animate corner radius change
2-4 seconds animate scale transformation
4-6 seconds animate rotation (hidden from the user)
How can I make the shape disappear from 4 - 6 seconds?

If you want to show fade-out effect on view, use
let hideAnimation = CABasicAnimation(keyPath: "opacity")
Instead of
let hideAnimation = CABasicAnimation(keyPath: "hidden") //bad!
If you don't want animation to reset your values after finish, simply add
imageGroupAnimations.fillMode = .forwards
imageGroupAnimations.isRemovedOnCompletion = false
to your CAAnimationGroup

The duration is not how long something stays true — it is how long it takes to perform a change.
So if you want the view to vanish suddenly, obviously you need to change hideAnimation.duration = 2.0 so that the duration is very short, say, 0.01.
This is a hideAnimation where the view turns invisible suddenly at the 4th second of the animation, and stays invisible until the end of the animation:
let hideAnimation = CABasicAnimation(keyPath: "hidden")
hideAnimation.fromValue = false
hideAnimation.toValue = true
hideAnimation.beginTime = 4.0
hideAnimation.duration = 0.01
hideAnimation.fillMode = .forwards
But I did not make any other changes because I still do not understand the complete effect that you want...

Related

Why is my Swift button animation growing back to full size?

What I expect: That when I press down on the button it shrinks to 75% and stays at its 75% shrunken size until I let go.
What is happening: The buttons shrinks to 75%, but as soon as it finishes the duration of the animation, while my finger is still pressed on the button, it grows back to its original value.
I am called the following when a button is "Touched Down".
import Foundation
import UIKit
extension UIButton {
func shrink() {
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
shrink.isRemovedOnCompletion = false
layer.add(shrink, forKey: nil)
}
}
The size resets because CABasicAnimation doesn't update the layer's underlying value, only the presentation layer. So when the animation ends, it goes back to using the underlying value.
The best way to deal with this is to set the transform scale before the animation starts so that it is the correct value at the end.
func shrink() {
transform = CGAffineTransform(scaleX: 0.75, y: 0.75) // Set final state
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
layer.add(shrink, forKey: nil)
}
You can also set the fillMode to .forwards and isRemovedOnCompletion to false, which will keep the presentation layer changes in place, but this doesn't update the layer's actual scale, so the button will keep the original scale for tap detection. (You need to set the transform's scale as above to correct it.)
func shrink() {
let shrink = CABasicAnimation(keyPath: "transform.scale")
shrink.fromValue = 1.0
shrink.toValue = 0.75
shrink.duration = 0.5
shrink.isRemovedOnCompletion = false
shrink.fillMode = .forwards
layer.add(shrink, forKey: nil)
}
You can also use a view animation which will have the same visual effect, but also update the scale:
UIView.animate(withDuration: 0.5) { [weak self] in
self?.transform = CGAffineTransform(scaleX: 0.75, y: 0.75)
}

Animation stops when UIView reappears on scrolling UITableView

I have an spinning UIView to show progress in each cell of a UITableView and I am using this function to animate UIViews:
func rotate360Degrees(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat.pi * 2
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.add(rotateAnimation, forKey: nil)
}
It works fine when the cells appears first but when you scroll the UITableView and the cells disappear and after that they shows up by scrolling again, their animations are stopped. I tried calling the method for them again after reappearing but it didn't work. what is wrong with my code?
UITableViewCell objects are reusable and you need to restore the animation in prepareForReuse: or tableView(_:willDisplay:forRowAt:) method.
func getRotate360DegreesAnimation(duration: CFTimeInterval = 1.0) -> CABasicAnimation {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = .pi * 2
rotateAnimation.duration = duration
rotateAnimation.repeatCount = .infinity
return rotateAnimation
}
func restoreAnimation() {
let animation = getRotate360DegreesAnimation()
layer.removeAllAnimations()
layer.add(animation, forKey: nil)
}

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")

CABasicAnimation chaining not working

I am having difficulties chaining multiple CABasicAnimations. I just want to make a button scale to 1.5 times it's size, then to 0.5 and fade away (while scaling down).
The problem is that only the scaling up works then the button goes back to the original state.
I would have used CGAffineTransformMakeScale but it creates a bug in iOS 7. On iOS 8 & 9 it works fine
I tried using a CAAnimationGroup but same result.
Here's the code:
let scaleUp = CABasicAnimation(keyPath: "transform.scale")
scaleUp.fromValue = 1
scaleUp.toValue = 1.5
scaleUp.duration = 0.2
scaleUp.beginTime = 0
scaleUp.removedOnCompletion = true
self.myButton.layer.addAnimation(scaleUp, forKey: "up")
let scaleDown = CABasicAnimation(keyPath: "transform.scale")
scaleDown.fromValue = 1
scaleDown.toValue = 0.5
scaleDown.duration = 0.2
scaleDown.beginTime = scaleUp.beginTime + scaleDown.duration
scaleDown.removedOnCompletion = true
self.myButton.layer.addAnimation(scaleDown, forKey: "down")
let fade = CABasicAnimation(keyPath: "opacity")
fade.fromValue = 1
fade.toValue = 0
fade.duration = 0.2
fade.beginTime = scaleUp.beginTime + scaleDown.duration
fade.removedOnCompletion = false
self.myButton.layer.addAnimation(fade, forKey: "fade")
that works for me quite well, feel free to play with the duration times, or other animation properties.
I tried to use your instructions for the animation, but it does not mean I have set all properties correctly for your expectations.
Swift (2.x)
func addGrowShrinkAndFadeOutAnimationToView(viewToAnimate: UIView) {
let easeInOutTiming = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
let viewScaleXAnimation = CAKeyframeAnimation(keyPath: "transform.scale.x")
viewScaleXAnimation.duration = 0.4
viewScaleXAnimation.values = [1.0 as Float, 1.5 as Float, 0.5 as Float]
viewScaleXAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewScaleXAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewScaleXAnimation, forKey:"Grow, Shrink and Fade out_ScaleX")
let viewScaleYAnimation = CAKeyframeAnimation(keyPath: "transform.scale.y")
viewScaleYAnimation.duration = 0.4
viewScaleYAnimation.values = [1.0 as Float, 1.5 as Float, 0.5 as Float]
viewScaleYAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewScaleYAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewScaleYAnimation, forKey:"Grow, Shrink and Fade out_ScaleY")
let viewOpacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
viewOpacityAnimation.duration = 0.4
viewOpacityAnimation.values = [1.0 as Float, 1.0 as Float, 0.0 as Float]
viewOpacityAnimation.keyTimes = [0.0 as Float, 0.5 as Float, 1.0 as Float]
viewOpacityAnimation.timingFunctions = [easeInOutTiming, easeInOutTiming]
viewToAnimate.layer.addAnimation(viewOpacityAnimation, forKey:"Grow, Shrink and Fade out_Opacity")
}
Is there a specific reason you want to use CA animations? I find manipulating constraints easier. I have added a button in storyboard and created outlets for the height and width constraints of the button.
#IBOutlet weak var okButton: UIButton!
#IBOutlet weak var widthConstraint: NSLayoutConstraint!
#IBOutlet weak var heightConstraint: NSLayoutConstraint!
var buttonWidth : CGFloat?
var buttonHeight : CGFloat?
override func viewDidLoad() {
super.viewDidLoad()
self.buttonHeight = self.heightConstraint.constant
self.buttonWidth = self.widthConstraint.constant
}
#IBAction func okButtonTapped(button: UIButton) {
let animationDuration = 2.0
self.heightConstraint.constant = self.buttonWidth! * 2.0
self.widthConstraint.constant = self.buttonHeight! * 2.0
UIView.animateWithDuration(animationDuration, animations: {
button.layoutIfNeeded()
}) { (finished) in
self.heightConstraint.constant = self.buttonWidth! * 0.5
self.widthConstraint.constant = self.buttonHeight! * 0.5
UIView.animateWithDuration(animationDuration, animations: {
button.alpha = 0.0
button.layoutIfNeeded()
}, completion: { (finished) in
button.hidden = true
})
}
}

CABasicAnimation reverse(backwards)

I'm a bit struggling with this simple line animation. I figured out how to pause it, but what I need is to be able to reverse animation back to starting point from the moment I call function resetAnimation().
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
let pathLayer = CAShapeLayer()
func lineAnimation() {
let path = UIBezierPath()
let screenWidth = self.view.bounds.width
let screenHeight = self.view.bounds.height
path.moveToPoint(CGPointMake(screenWidth, screenHeight / 2))
path.addLineToPoint(CGPointMake(screenWidth - screenWidth, screenHeight / 2))
self.pathLayer.frame = self.view.bounds
self.pathLayer.path = path.CGPath
self.pathLayer.strokeColor = UIColor.whiteColor().CGColor
self.pathLayer.fillColor = nil
self.pathLayer.lineWidth = 3.0
self.pathLayer.lineCap = kCALineCapRound
self.pathLayer.speed = 1
self.view.layer.addSublayer(pathLayer)
self.pathAnimation.duration = 5.0
self.pathAnimation.fromValue = 0.0
self.pathAnimation.toValue = 1.0
pathLayer.addAnimation(pathAnimation, forKey: "animate")
}
func pauseAnimation() {
let pausedTime = pathLayer.convertTime(CACurrentMediaTime(), fromLayer: nil)
pathLayer.speed = 0
pathLayer.timeOffset = pausedTime
}
func resetAnimation() {
}
You just need to create a new animation and remove the old one. Your starting point for the new animation will be the current value of that property in your presentation layer. I'd also recommend setting the frame of your shape layer to the bounds of the actual bezier shape instead of the entire view - its a good habit to be in when you start moving things around and scaling/rotating/etc. Otherwise you're gonna be faced with a bunch of funky conversions or anchor point changes.
Here's what I'd do:
let pathLayer = CAShapeLayer()
// first, separate your drawing code from your animation code.
// this way you can call animations without instantiating new objects
func drawLine() {
let path = UIBezierPath()
// draw your path with no position translation.. move the layer
path.moveToPoint(CGPointMake(view.bounds.width, 0))
path.addLineToPoint(CGPointMake(0, 0))
pathLayer.frame = path.bounds
// this line sets the position of the layer appropriately
pathLayer.position = view.bounds.width - pathLayer.bounds.width / 2
pathLayer.path = path.CGPath
pathLayer.strokeColor = UIColor.whiteColor().CGColor
pathLayer.fillColor = nil
pathLayer.lineWidth = 3.0
pathLayer.lineCap = kCALineCapRound
view.layer.addSublayer(pathLayer)
}
func lineAnimation() {
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = 5.0
pathAnimation.fromValue = 0.0
pathAnimation.toValue = 1.0
pathLayer.addAnimation(pathAnimation, forKey: "strokeEnd")
}
func reverseAnimation() {
let revAnimation = CABasicAnimation(keyPath: "strokeEnd")
revAnimation.duration = 5.0
// every Core Animation has a 'presentation layer' that contains the animated changes
revAnimation.fromValue = pathLayer.presentationLayer()?.strokeEnd
revAnimation.toValue = 0.0
pathLayer.removeAllAnimations()
pathLayer.addAnimation(revAnimation, forKey: "strokeEnd")
}
Keep in mind you'll also want to set your properties so you retain the end values of the animation.
You could also use the autoreverses property of CAMediaTiming protocol, which is complied by CABasicAnimation