CABasicAnimation reverse(backwards) - swift

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

Related

How to achieve this animation with multiple CAShapeLayer

I'm having a lot of trouble exporting a video with animated layers.
The final goal is to export to a video (that's why I need this done with CALayers) a moving CAShapeLayer around the screen and a line following its trace.
First of all, I have an array of CGPoint with all the coordinate points which a CAShapeLayer should use to animate itself. To create the animation I'm using a CAKeyframeAnimation.
let values = [CGPoint(x: 50, y: 100),
CGPoint(x: 100, y: 150)] //Example values
let animation = CAKeyframeAnimation()
animation.keyPath = "position"
animation.values = values
animation.duration = videoLength
With this, I'm able to show the CALayer moving around the screen. It's 1 value per frame of the video, it's a 1:1 value.
The real problem comes with adding a line that shows the path of the CAShapeLayer.
What I think I might do, is to create lines with UIBezierPath, animate them and add each line
lines.forEach { (line) in
let path = UIBezierPath()
let shapeLayer = CAShapeLayer()
for (index, point) in line.points.enumerated() {
if index == 0 {
path.move(to: point)
} else {
path.addLine(to: point)
}
}
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = line.color.cgColor
shapeLayer.lineWidth = 4
shapeLayer.position = animatedLayer.position
shapeLayer.path = path.cgPath
shapeLayer.opacity = 0
shapeLayers.append(shapeLayer)
}
And to animate the lines...
var timeOffset: Double = 0
let eachLineTime = totalTime / Double(shapeLayers.count)
for shapeLayer in shapeLayers {
CATransaction.begin()
mainLayer.addSublayer(shapeLayer)
let strokeAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeAnimation.fromValue = 0
strokeAnimation.toValue = 1
strokeAnimation.duration = eachLineTime
strokeAnimation.beginTime = CACurrentMediaTime() + timeOffset
let opacityAnimation = CABasicAnimation(keyPath: "opacity")
opacityAnimation.fromValue = 0
opacityAnimation.toValue = 1
opacityAnimation.duration = eachLineTime
opacityAnimation.beginTime = CACurrentMediaTime() + timeOffset
CATransaction.setCompletionBlock {
shapeLayer.strokeColor = UIColor.orange.cgColor
shapeLayer.opacity = 1
}
shapeLayer.add(strokeAnimation, forKey: nil)
shapeLayer.add(opacityAnimation, forKey: nil)
timeOffset += eachLineTime
CATransaction.commit()
}
I need them to be different lines because I want to change the color of each line when it finishes each animation.
I'm getting the next output in Swift Playgrounds
But I'm not getting anything when I export the video in the app.
How can I simply add a trail of the first CAShapeLayer to see the (square in this case) moving path?
How can I move the CABasicAnimation with the same timing of the CAKeyframeAnimation?

Swift- How to find width/height of a Presentation Layer that is being scaled and rotated?

So, I have a CALayer that is being rotated and scaled using CABasicAnimation. Since the layer is being rotated, frame returns the frame containing the highest to lowest x and y values(so this will be larger than the actual presented layer). And, while bounds would work if scaling does not occur, the bounds of the layer do not. Here is my code for my custom UIView subclass:
func rotateCircleView(duration: CFTimeInterval = 1.0) {
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.fromValue = 0.0
rotateAnimation.toValue = CGFloat(M_PI * 2.0)
rotateAnimation.duration = duration
rotateAnimation.repeatCount = Float.infinity
self.layer.addAnimation(rotateAnimation, forKey: rotateKey)
}
func shrinkCircleView(duration: CFTimeInterval = 1.0, to: Double) {
let from = self.getCurrentAnimationScale()
let shrinkAnimation = CABasicAnimation(keyPath: "transform.scale")
shrinkAnimation.additive = true
shrinkAnimation.fromValue = from - to
shrinkAnimation.toValue = 0
shrinkAnimation.duration = duration
shrinkAnimation.removedOnCompletion = true
let scaleTransform = CATransform3DMakeScale(CGFloat(to), CGFloat(to), 1.0)
layer.transform = scaleTransform
layer.addAnimation(shrinkAnimation, forKey: "shrinkAnimation")
}
func getCurrentAnimationScale() -> Double{
if let temp = self.layer.presentationLayer(){
// return scale of presentationLayer
} else{
// return scale of self.layer
}
}

Swift - how to animatedly draw line NOT rect? Can only draw rect?

I have looked around about this before but have yet to find an answer. I've been at this for a while and need to know how to animate a LINE as opposed to a rectangle.
From what I can see the animation is very different going from a stroke to a box. Just need some pointers here-
I have a border that is drawn on view load (not with an animation, it just draws it) here:
override public func drawRect(rect: CGRect){
super.drawRect(rect)
let borderColor = self.hasError! ? kDefaultActiveColor : kDefaultErrorColor
let textRect = self.textRectForBounds(rect)
let context = UIGraphicsGetCurrentContext()
let borderlines : [CGPoint] = [CGPointMake(0, CGRectGetHeight(textRect) - 1),
CGPointMake(CGRectGetWidth(textRect), CGRectGetHeight(textRect) - 1)]
if self.enabled {
CGContextBeginPath(context);
CGContextAddLines(context, borderlines, 2);
CGContextSetLineWidth(context, 1.3);
CGContextSetStrokeColorWithColor(context, borderColor.CGColor);
CGContextStrokePath(context);
I got this from another answer and am trying to take it apart. I need the line to animate/grow out from its centre when the view loads, as in grow to the same width and size as it is drawn here but animated.
I do not know how to accomplish this, however another answer achieved the effect I need by setting the border to a size of zero here:
self.activeBorder = UIView(frame: CGRectZero)
self.activeBorder.backgroundColor = kDefaultActiveColor
//self.activeBorder.backgroundColor = kDefaultActiveBorderColor
self.activeBorder.layer.opacity = 0
self.addSubview(self.activeBorder)
and animating using:
self.borderlines.transform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
self.activeBorder.layer.opacity = 1
CATransaction.begin()
self.activeBorder.layer.transform = CATransform3DMakeScale(CGFloat(0.03), CGFloat(1.0), 1)
let anim2 = CABasicAnimation(keyPath: "transform")
let fromTransform = CATransform3DMakeScale(CGFloat(0.01), CGFloat(1.0), 1)
let toTransform = CATransform3DMakeScale(CGFloat(1.0), CGFloat(1.0), 1)
anim2.fromValue = NSValue(CATransform3D: fromTransform)
anim2.toValue = NSValue(CATransform3D: toTransform)
anim2.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
anim2.fillMode = kCAFillModeForwards
anim2.removedOnCompletion = false
self.activeBorder.layer.addAnimation(anim2, forKey: "_activeBorder")
CATransaction.commit()
This seems to be using entirely different code than my original drawRect and I don't know how to combine the two. Where am I going wrong here? How can I draw out from the centre my first border as I do the second, active border?
Use CAShapeLater, UIBezierPath and CABasicAnimation to animatedly draw line.
Here is the sample code:
class SampleView: UIView {
override public func drawRect(rect: CGRect) {
let leftPath = UIBezierPath()
leftPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
leftPath.addLineToPoint(CGPointMake(0, CGRectGetHeight(frame)))
let rightPath = UIBezierPath()
rightPath.moveToPoint(CGPointMake(CGRectGetMidX(frame), CGRectGetHeight(frame)))
rightPath.addLineToPoint(CGPointMake(CGRectGetWidth(frame), CGRectGetHeight(frame)))
let leftShapeLayer = CAShapeLayer()
leftShapeLayer.path = leftPath.CGPath
leftShapeLayer.strokeColor = UIColor.redColor().CGColor;
leftShapeLayer.lineWidth = 1.3
leftShapeLayer.strokeEnd = 0
layer.addSublayer(leftShapeLayer)
let rightShpaeLayer = CAShapeLayer()
rightShpaeLayer.path = rightPath.CGPath
rightShpaeLayer.strokeColor = UIColor.redColor().CGColor;
rightShpaeLayer.lineWidth = 1.3
rightShpaeLayer.strokeEnd = 0
layer.addSublayer(rightShpaeLayer)
let drawLineAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawLineAnimation.toValue = NSNumber(float: 1)
drawLineAnimation.duration = 1
drawLineAnimation.fillMode = kCAFillModeForwards
drawLineAnimation.removedOnCompletion = false
leftShapeLayer.addAnimation(drawLineAnimation, forKey: nil)
rightShpaeLayer.addAnimation(drawLineAnimation, forKey: nil)
}
}
The code in viewDidLoad of ViewController
// ...
let sampleView = SampleView()
sampleView.frame = CGRectMake(10, 60, 240, 50)
sampleView.backgroundColor = UIColor.lightGrayColor()
view.addSubview(sampleView)
Here is the capture:
Note: If you run the code in iOS9 Simulator (I have not tested the earlier version), the animation may get blocked. Choose to run it in a real device or in iOS10 Simulator.

CAShapelayer Duplicates

I'm trying to add a CAShapelayer once every 20ms to given x and y coordinates. I would like the shape to fade away over a second (like a tracer). The function I have created works, the shape is created in the correct location and fades away. But I am getting extra shapes left behind cluttering up the screen.
func shadowBall (x: CGFloat, y: CGFloat){
let xpos : CGFloat = ((self.frame.width/2) + x)
let ypos : CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius :CGFloat = 4
let shadowBallPath : UIBezierPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.whiteColor().CGColor
shadowBall.lineWidth = 0.5
let animation: CABasicAnimation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0;
animation.repeatCount = 0;
animation.removedOnCompletion = true
animation.additive = false
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
The problem is that when the animation finishes, it restores the strokeColor to the original color. You should really set the strokeColor of the original shape layer to be clearColor(), that way, when you finish animating from whiteColor() to clearColor(), it will remain at clearColor().
You can also set the layer's fillMode to kCAFillModeForwards and set removedOnCompletion to false and that will have the layer preserve its "end of animation" state. But I personally would just set the strokeColor as outlined above, as using removedOnCompletion of true interferes with animationDidStop (see below).
Also, I might suggest that you also remove the layer once it's done with the animation so it doesn't continue to consume memory although it's no longer visible.
func shadowBall (x: CGFloat, y: CGFloat) {
let xpos: CGFloat = ((self.frame.width/2) + x)
let ypos: CGFloat = ((self.frame.height/2) + y)
let shadowBall = CAShapeLayer()
let shadowBalllRadius: CGFloat = 4
let shadowBallPath = UIBezierPath(ovalInRect: CGRect(x: xpos, y: ypos, width: CGFloat(shadowBalllRadius*2), height: CGFloat(shadowBalllRadius*2)))
shadowBall.path = shadowBallPath.CGPath
shadowBall.fillColor = UIColor.clearColor().CGColor
shadowBall.strokeColor = UIColor.clearColor().CGColor
shadowBall.lineWidth = 0.1
let animation = CABasicAnimation(keyPath: "strokeColor")
animation.fromValue = UIColor.whiteColor().CGColor
animation.toValue = UIColor.clearColor().CGColor
animation.duration = 1.0
animation.repeatCount = 0
animation.removedOnCompletion = true
animation.additive = false
animation.delegate = self
animation.setValue(shadowBall, forKey: "animationLayer")
self.layer.addSublayer(shadowBall)
shadowBall.addAnimation(animation, forKey: "strokeColor")
}
override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let layer = anim.valueForKey("animationLayer") as? CALayer {
layer.removeFromSuperlayer()
}
}
See How to remove a CALayer-object from animationDidStop?

CABasicAnimation ignores duration

I can't seem to control the duration of a simple Core Animation whether I use a CATransation or not.
The transaction completion code fires after my duration setting like I'd expect, but the rotation animation always takes about 0.5 seconds no matter what duration it's set to.
But if I comment out the last line that sets the transform, the animation moves at the proper speed, but then snaps back to the original rotation.
What am I missing?
class MyControl: NSControl {
var shapeLayer = CAShapeLayer()
required override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
self.wantsLayer = true
self.layerContentsRedrawPolicy = NSViewLayerContentsRedrawPolicy.OnSetNeedsDisplay
// create a triangle shape
let path = NSBezierPath()
path.moveToPoint(CGPointMake(45.0, 25.0))
path.lineToPoint(CGPointMake(5.0, 5.0))
path.lineToPoint(CGPointMake(5.0, 45.0))
path.lineToPoint(CGPointMake(45.0, 25.0))
path.closePath()
shapeLayer.frame = self.bounds
shapeLayer.path = path.toCGPath()
shapeLayer.fillColor = NSColor(red: 1.0, green: 0.0, blue: 0.0, alpha: 1.0).CGColor
self.layer!.addSublayer(shapeLayer)
}
override func mouseUp(theEvent: NSEvent){
let duration = 2.0
let endAngle = -90.0
let startAngle = shapeLayer.valueForKey("transform.rotation")
CATransaction.begin()
CATransaction.setAnimationDuration(duration)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut))
CATransaction.setCompletionBlock({
Swift.print("this fires after 2 seconds")
})
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.removedOnCompletion = true
rotateAnimation.autoreverses = false
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
rotateAnimation.duration = duration
rotateAnimation.fromValue = startAngle
rotateAnimation.toValue = endAngle.degreesToRadians
shapeLayer.addAnimation(rotateAnimation, forKey: rotateAnimation.keyPath)
CATransaction.commit()
// mystery line...
shapeLayer.transform = CATransform3DMakeRotation(endAngle.degreesToRadians, 0.0, 0.0, 1.0)
}
}
On a side note, I made use of a couple of useful extensions... toCGPath in NSBezierPath and degreesToRadians for CGFloat.
The reason why you are seeing this strange behaviour is usage of explicit CATransaction begin--commit. This will in turn add a nested CATransaction within the one already owned by the run loop. So what happens is, your inner transaction gets committed, but before the associated animations finish, you have an immediate new value for the transform. This will cause this glitch where your layer will just quickly rotate and vanish.
Instead of this, the straight forward way to do is
1. Create your animation
2. Set a delegate for your animation
3. In delegate call back set the target value you would want the layer to remain in.
override func mouseUp(theEvent: NSEvent){
let duration = 2.0
let endAngle = -90.0
let startAngle = shapeLayer.valueForKey("transform.rotation")
//Set up animation
let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
rotateAnimation.removedOnCompletion = true
rotateAnimation.autoreverses = false
rotateAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
rotateAnimation.duration = duration
rotateAnimation.fromValue = startAngle
rotateAnimation.toValue = endAngle * 3.142/180
//Setup delegate
rotateAnimation.delegate = self
shapeLayer.addAnimation(rotateAnimation, forKey: #"rotationAnimation")
}
public override func animationDidStop(anim: CAAnimation, finished flag: Bool){
shapeLayer.transform = CATransform3DMakeRotation(CGFloat(-90.0 * 3.142/180), 0.0, 0.0, 1.0)
}
P.S: I have not run this code, and also note sure what value you want to set for the transform of your layer once the animation completes.
I think the problem is because your startAngle and endAngle. Try to change it like this. This works for me
private let startAngle: CGFloat = CGFloat(-M_PI_2)
private let endAngle: CGFloat = CGFloat(-M_PI_2) + CGFloat(M_PI * 2)