How do I animate opacity using swift? - 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
}

Related

How to distinguish between animations in animationDidStop (value for Key) in xCode (swift)

I have two animations and I am trying to figure out which one ended, and then do what I want to do with it. I know i can distinguish between animations using valueForKey, but can't figure out the exact way.
func myanimation1() {
let pulse1 = CABasicAnimation(keyPath: "transform.scale")
pulse.duration = 0.25
pulse.fromValue = 1
pulse.toValue = 1.05
pulse.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pulse.autoreverses = true
pulse.repeatCount = 2
// pulse.isRemovedOnCompletion = false
myImageView1.layer.add(pulse, forKey: myKey1)
}
func myanimation2() {
let pulse2 = CABasicAnimation(keyPath: "transform.scale")
pulse2.duration = 0.5
pulse2.fromValue = 1.3
pulse2.toValue = 1.5
pulse2.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut)
pulse2.autoreverses = true
pulse2.repeatCount = 4
// pulse2.isRemovedOnCompletion = false
myImageView2.layer.add(pulse, forKey: myKey2)
}
Now, how to do this bit please?
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
//if pulse1 ended { do something }
//if pulse2 ended { do something else }
}
Any help greatly appreciated.
Here's a really cool secret: you can use key-value coding to give your animation a name or other identifying mark.
let pulse2 = CABasicAnimation(keyPath: "transform.scale")
pulse2.setValue("pulse2", forKey:"name")
In the delegate method, you receive the animation, so you can check that name:
if let name = anim.value(forKey:"name"), name == "pulse2" ...

CAAnimationGroup timing

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...

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

How to animate borderColor change in swift

For some reason this isn't working for me:
let color = CABasicAnimation(keyPath: "borderColor")
color.fromValue = sender.layer.borderColor;
color.toValue = UIColor.redColor().CGColor;
color.duration = 2;
color.repeatCount = 1;
sender.layer.addAnimation(color, forKey: "color and width");
I'm not getting any animation to occur.
Swift 4 UIView extension:
extension UIView {
func animateBorderColor(toColor: UIColor, duration: Double) {
let animation = CABasicAnimation(keyPath: "borderColor")
animation.fromValue = layer.borderColor
animation.toValue = toColor.cgColor
animation.duration = duration
layer.add(animation, forKey: "borderColor")
layer.borderColor = toColor.cgColor
}
}
And then just use it by writing:
myView.animateBorderColor(toColor: .red, duration: 0.5)
You have to use the same key name. You also forgot to add a border width and color to your layer before animating it. Try like this:
let color = CABasicAnimation(keyPath: "borderColor")
#IBAction func animateBorder(sender: AnyObject) {
color.fromValue = UIColor.greenColor().CGColor
color.toValue = UIColor.redColor().CGColor
color.duration = 2
color.repeatCount = 1
sender.layer.borderWidth = 2
sender.layer.borderColor = UIColor.greenColor().CGColor
sender.layer.addAnimation(color, forKey: "borderColor")
}
(Swift 5, Xcode 11, iOS 13)
For anyone who is wanting to change border color and width at the same time, the following code is working for me & the animation looks very smooth. Maybe someone else knows of a way to combine both into one?
let borderColorAnimation: CABasicAnimation = CABasicAnimation(keyPath: "borderColor")
borderColorAnimation.fromValue = layer.borderColor
borderColorAnimation.toValue = toColor.cgColor
borderColorAnimation.duration = animationDuration
layer.add(borderColorAnimation, forKey: "borderColor")
layer.borderColor = toColor.cgColor
let borderWidthAnimation: CABasicAnimation = CABasicAnimation(keyPath: "borderWidth")
borderWidthAnimation.fromValue = layer.borderWidth
borderWidthAnimation.toValue = toWidth
borderWidthAnimation.duration = animationDuration
layer.add(borderWidthAnimation, forKey: "borderWidth")
layer.borderWidth = toWidth
I created a Swift 4 function extension to CALayer for this, to which you can pass the starting and ending UIColors as well as the duration for the animation:
extension CALayer {
func animateBorderColor(from startColor: UIColor, to endColor: UIColor, withDuration duration: Double) {
let colorAnimation = CABasicAnimation(keyPath: "borderColor")
colorAnimation.fromValue = startColor.cgColor
colorAnimation.toValue = endColor.cgColor
colorAnimation.duration = duration
self.borderColor = endColor.cgColor
self.add(colorAnimation, forKey: "borderColor")
}
Note that for this to work, you should have already set a borderWidth and then call animateBorderColor:
yourView.layer.borderWidth = 1.5
yourView.layer.animateBorderColor(from: UIColor.green, to: UIColor.red, withDuration: 2.0)
I don't know why, but for some reason calling:
color.fromValue = sender.layer.borderColor
doesn't work. The color isn't being read correctly or something. I changed it to:
let color = CABasicAnimation(keyPath: "borderColor");
color.fromValue = UIColor.greenColor().CGColor;
color.toValue = UIColor.redColor().CGColor;
color.duration = 2;
color.repeatCount = 1;
sender.layer.addAnimation(color, forKey: "color and width");
And then things started working as expected.