Is it possible to subclass CAAnimation? - ios

I just want to animate a layer's position property from point A to point B through an arc with center point C and radius R. So simple and yet I feel like I am trying to bend Core Animation in half to do this.
It seems like the ideal way would be to simply create a custom CAPropertyAnimation subclass, something like:
let anim = MyArcPropertyAnimation(keyPath: "position", center: CGPoint, radius: CGFloat)
anim.fromValue = fromPoint
anim.toValue = toPoint
myLayer.addAnimation(anim, forKey: "positionArcAnimation")
But I can't find any info on subclassing CAAnimation or any of it's derivatives. The same seems to go for CAValueFunction which otherwise seems promising as well.
Note: I got it running using POP easily enough but found that, as warned by a facebook engineer, the performance was too slow for my usage.

It seems implied from the documentation that CAAnimation is not designed for subclassing. Also, most animations are performed in the window server (called backboardd) and you're definitely not going to be able to get backboardd to execute your code.
Anyway, it sounds like you can get the effect you want using a CAKeyframeAnimation. Set the path property of the CAKeyframeAnimation to the arc you want the layer to follow. Playground example:
import UIKit
import XCPlayground
let view = UIView(frame: CGRectMake(0, 0, 400, 400))
view.backgroundColor = .whiteColor()
let mover = UIView(frame: CGRectMake(0, 0, 10, 10))
mover.backgroundColor = .redColor()
view.addSubview(mover)
XCPlaygroundPage.currentPage.liveView = view
let animation = CAKeyframeAnimation(keyPath: "position")
let path = UIBezierPath(arcCenter: CGPointMake(200, 200), radius: 160, startAngle: 1, endAngle: 5, clockwise: true)
animation.path = path.CGPath
animation.duration = 2
animation.calculationMode = kCAAnimationPaced
mover.layer.addAnimation(animation, forKey: "position")
mover.center = path.currentPoint
Result:

Related

iOS Swift how to synchronise chart drawing line, gradient and circle animations?

I try to synchronise animations of chart line, circle and gradient.
Here is fancy chart gif with not synchronised animations :/
Run all animations code:
CATransaction.begin()
CATransaction.setAnimationDuration(5.0)
CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeInEaseOut))
self?.animateLineDrawing()
self?.animateGradient()
self?.animateCircle()
CATransaction.commit()
Line drawing animation:
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0.0
animation.toValue = 1.0
lineShapeLayer.add(animation, forKey: "drawLineAnimation")
lineShapeLayer.path = curvedPath.cgPath
I have gradientContainer layer with gradientLayer as sublayer. I move only container mask layer over gradient Layer:
let animationPath = UIBezierPath(rect: CGRect(x: firstPoint.x,
y: chartRect.origin.y,
width: 0,
height: chartRect.height))
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = animationPath.cgPath
animation.toValue = UIBezierPath(rect: CGRect(x: firstPoint.x,
y: chartRect.origin.y,
width: chartRect.width,
height: chartRect.height)).cgPath
animation.isRemovedOnCompletion = false
animation.fillMode = CAMediaTimingFillMode.forwards
animation.delegate = self
gradientContainerLayer.mask?.add(animation, forKey: nil)
Circle animation over chart path:
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = viewModel.curvedPath(frame)?.cgPath
animation.delegate = self
circleLayer.add(animation, forKey: nil)
Gradient animation is not synchronised because distance over path is different from straight line, anyone has idea how to synchronise this?
Why circle animation timing is not equal to line animation? It look like begin and end of animations are equal so timing functions are different, why?
Instead of using basic and property animation switch all on CAKeyframeAnimation. I personally found them way simpler to tweak and more flexible in general.
Calculate position for every element to be in same x coordinate at same time point.
You should consider adding minimum 1 point in each point of interest (change between decreasing and increasing of y coordinate) so you do not cut local extremes.

How to use UIViewAnimationCurve options in CAAnimation

I am using a CAAnimation in a CAShapeLayer to simply animate a line that strokes from left to right (it's just a cut-down version of a more complex program). I have slowed it down to show that the beginning quarter is almost instantaneous, and then the rest animates normally.
I have seen that one can use UIView.animate's options to set the UIViewAnimationCurve options (for instance, .EaseIn or .Linear). Is it possible to set these options when embedding a CAAnimation in a CAShapeLayer? And is it necessary to create the animation every time you want to run it, or is there a "run animation" command?
Below is my cut-down code:
// the containing UIView is called mainView
mainView.layer.sublayers = nil
// create a line using a UIBezierPath
let path = UIBezierPath()
path.move(to: CGPoint(x: 100, y: 100))
path.addLine(to: CGPoint(x: 300, y: 100))
// create a CAShapeLayer
let shapeLayer = CAShapeLayer()
shapeLayer.frame = CGRect(x: 0, y: 0, width: 1024, height: 1024)
shapeLayer.strokeColor = UIColor.green.cgColor
shapeLayer.lineWidth = 100
shapeLayer.path = path.cgPath
// create the animation
mainView.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "LetterStroke")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 10
shapeLayer.add(animation, forKey: "animation")
Many thanks in advance.
You should use the timingFunction property of CABasicAnimation. There is a constructor that accepts these predefined timing functions like
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
The problem was that the line animation appeared to start a quarter of the way in. I neglected to put what turned out to be the offending line of code in my question.
shapeLayer.lineCap = kCALineCapRound
This, combined with the lineWidth of 100, made the line start approximately a quarter of the way from the beginning. I made the lineWidth smaller, and it worked fine.

Animated CGAffineTransform(rotate) of a UIBezier path that is under func draw()

I want to make an animated of a path with CGAffineTransform(rotationAngle:) that is under a UIView`s draw. I couldn't find any way to do it...
This is my code:
public override func draw(_ rect: CGRect) {
//Drawing a dot at the center
let dotRadius = max(bounds.width/15, bounds.height/15)
let dotWidth:CGFloat = 10
let dotCenter = CGPoint(x:bounds.width/2, y: bounds.height/2)
let dotStartAngle: CGFloat = 0
let dotEndAngle: CGFloat = CGFloat(2 * M_PI) // Ļ€
var path = UIBezierPath(arcCenter: dotCenter,
radius: dotRadius/2,
startAngle: dotStartAngle,
endAngle: dotEndAngle,
clockwise: true)
path.lineWidth = dotWidth
counterColor.setStroke()
counterColor.setFill()
path.stroke()
path.fill()
arrowLayer.frame = CGRect(x:bounds.width/2, y: bounds.height/2, width: bounds.width, height: bounds.height)
arrow.path = UIBezierPath(rect: CGRect(x: 0, y:0 - 1, width: -250, height: 1)).cgPath
arrow.backgroundColor = UIColor.red.cgColor
arrow.fillColor = UIColor.clear.cgColor
arrow.strokeColor = counterColor.cgColor
arrow.anchorPoint = CGPoint(x:0.5, y:0.5)
arrow.lineWidth = 3
arrow.setAffineTransform(CGAffineTransform(rotationAngle:self.radians(arrowDegree)))
arrowLayer.addSublayer(arrow)
self.layer.addSublayer(arrowLayer)
}
And this is what I'm trying to do:
func rotateButton() {
UIView.animate(withDuration: 1, animations: {
self.arrow.setAffineTransform(CGAffineTransform(rotationAngle:self.radians(self.arrowDegree + 10)))
self.setNeedsDisplay()
})
}
I don't know if I was specific.. tell me if more information is needed.
I would recommend that you separate the animation from the drawing. Overriding draw() is generally suited for custom drawing but ill-suited for any type of animation.
Based on the properties that's being assigned to arrow it looks like it's a CAShapeLayer. This is good because it means that there is already some separation between the two.
However, it's not a good idea to add subviews or sublayers inside of draw(). It should only be used for drawing. Otherwise these things are going to get re-added every time the view is redrawn. In this case it's less noticeable because the same layer is added over and over. But it should still be avoided.
You can still use draw() to render the centered dot. But it's not expected to be part of the animation.
As for the rotation animation; unless I greatly misremember the underlying mechanics, standalone layers (those that aren't backing a view (most likely you've created them)) don't care about the UIView animation context. Instead you would use a CoreĀ Animation level API to animate them. This could either be done by changing the property within a CATransaction, or by creating a CABasicAnimation from one angle to the other and adding it to the layer.
Note that adding an animation doesn't change the "model" value (the properties of the layer), and that once the animation finishes the layer would render as it did before the animation was added. If the layer is expected to animate to a value and stay there you would both add the animation and update the layer property.

How to make a filling progress circle in iOS?

I need to make a progress circle like this:
I have created a CAShapeLayer for the circle, and animate its strokeEnd property like this:
pieShape = CAShapeLayer()
pieShape.path = UIBezierPath(arcCenter: CGPointMake(29, 29), radius: 27.0, startAngle: CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true).CGPath
pieShape.fillColor = UIColor.clearColor().CGColor
pieShape.strokeColor = UIColor.blackColor().CGColor
pieShape.lineWidth = 4.0
self.layer.addSublayer(pieShape)
let countDownAnimation = CABasicAnimation(keyPath: "strokeEnd")
countDownAnimation.duration = durationInSeconds
countDownAnimation.removedOnCompletion = false
countDownAnimation.fromValue = NSNumber(float: 0.0)
countDownAnimation.toValue = NSNumber(float: 1.0)
countDownAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
self.pieShape.strokeStart = 1.0
self.pieShape.addAnimation(countDownAnimation, forKey: "drawCircleAnimation")
But, this not what I want. I need to animate the circle filling, not only the border. Thank you.
The easiest way is to cheat, and animate just the stroke, but make it appear to be animating the fill of the circle. For example, if you wanted a circle of radius 30, use a radius of 15 instead, but a lineWidth of 30, so that the stroke effectively covers from the center to the desired radius of 30.
There are more complicated methods using CADisplayLink, and updating it yourself, but the above is pretty easy way to achieve the desired effect.

UIImageView follow circular path in Single View Application, how?

I'm working on a single view application, not SpriteKit, in Swift, and I have a UIImageView called obstacle, that I want to follow a circular path (assume the circle diameter is 100).
How can this be done? Can anyone help me? I've seen lots of answers but they're all for people using SpriteKit, and other ones that I have found are in Objective-C and I can't find certain methods in Swift that they're referencing...
Thanks!
Try out following code
let curvedPath = CGPathCreateMutable()
let circleContainer = CGRectMake(100, 100, 200, 200) // change according to your needs
CGPathAddEllipseInRect(curvedPath, nil, circleContainer)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = curvedPath
animation.rotationMode = kCAAnimationRotateAuto
animation.repeatCount = Float.infinity
animation.duration = 5.0
obstacle.layer.addAnimation(animation, forKey: "animate position along path")
Try out this code. You can rotate it to anti-clockwise or clockwise both. Just set UIBezierPath clockwise parameter as true or false as required.
let circlePath = UIBezierPath(arcCenter: CGPointMake(CGRectGetMidX(view.frame), CGRectGetMidY(view.frame)), radius: 100, startAngle: 0, endAngle:CGFloat(M_PI)*2, clockwise: true)
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = circlePath.CGPath
animation.duration = 4
animation.additive = true
animation.repeatCount = HUGE
animation.calculationMode = kCAAnimationPaced
animation.rotationMode = kCAAnimationRotateAuto
view.layer.addAnimation(animation, forKey: "orbit")

Resources