How to use UIViewAnimationCurve options in CAAnimation - ios

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.

Related

Swift: How to set CAShapeLayer() progress value

I just create an IOS app where I implement a round progress bar with CAShapeLayer().
And know I want to set in each second the progress value to +1 is this posible?
I would just like to create a progress bar like this one that leaps forward a value every second:
Like this
Best Regards!
CAShapeLayer has animatable strokeStart and strokeEnd properties. You can directly set these and they will be animated automatically, or you can control the animation yourself like any other animatable CALayer property:
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 1
shape.add(animation, forKey: nil)
This will fill the progress indicator in 1 second.
For a stroke end animation to work, you can't use a CGPath which is just a circle - you need an arc with a start and an end, like this:
let path = CGMutablePath()
path.addArc(
center: CGPoint(x: 150, y: 150),
radius: 100,
startAngle: -0.5 * .pi,
endAngle: 1.5 * .pi,
clockwise: false
)

How to position Layers in swift programmatically?

I am trying to position a circular progress bar but I am failing constantly. I guess I am not understanding the concept of frames and views and bounds. I found this post on stack overflow that shows me exactly how to construct a circular progress bar.
However, in the post, a circleView was created as UIView using CGRect(x: 100, y: 100, width: 100, height: 100)
In my case, manually setting the x and y coordinates is obv a big no. So the circleView has to be in the centre of it's parent view.
Here is my view hierarchy:
view -> view2ForHomeController -> middleView -> circleView
So everything is positioned using auto layout. Here is the problem:The circleView is adding properly and it positions itself at the x and y value I specify but how can I specify the x and y values of the center of middleView. I tried the following but the center values are returned as 0.0, 0.0.
self.view.addSubview(self.view2ForHomeController)
self.view2ForHomeController.fillSuperview()
let xPosition = self.view2ForHomeController.middleView.frame.midX
let yPostion = self.view2ForHomeController.middleView.frame.midY
print(xPosition) // ------- PRINTS ZERO
print(yPostion) // ------- PRINTS ZERO
let circle = UIView(frame: CGRect(x: xPosition, y: yPostion, width: 100, height: 100))
circle.backgroundColor = UIColor.green
self.view2ForHomeController.middleView.addSubview(circle)
var progressCircle = CAShapeLayer()
progressCircle.frame = self.view.bounds
let lineWidth: CGFloat = 10
let rectFofOval = CGRect(x: lineWidth / 2, y: lineWidth / 2, width: circle.bounds.width - lineWidth, height: circle.bounds.height - lineWidth)
let circlePath = UIBezierPath(ovalIn: rectFofOval)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.cgPath
progressCircle.strokeColor = UIColor.white.cgColor
progressCircle.fillColor = UIColor.clear.cgColor
progressCircle.lineWidth = 10.0
progressCircle.frame = self.view.bounds
progressCircle.lineCap = CAShapeLayerLineCap(rawValue: "round")
circle.layer.addSublayer(progressCircle)
circle.transform = circle.transform.rotated(by: CGFloat.pi/2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.1
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.easeIn)
progressCircle.add(animation, forKey: nil)
Frame is in terms of superview/superlayer coordinates. If you are going to say
circle.layer.addSublayer(progressCircle)
then you must give progressCircle a frame in terms of the bounds of circle.layer.
As for centering, in general, to center a sublayer in its superlayer you say:
theSublayer.position = CGPoint(
x:theSuperlayer.bounds.midX,
y:theSuperlayer.bounds.midY)
And to center a subview in its superview you say:
theSubview.center = CGPoint(
x:theSuperview.bounds.midX,
y:theSuperview.bounds.midY)
The bounds are the layer/view's coordinates in its local coordinate system. The frame is the layer/view's coordinates in its parents coordinate system. Since layers do not participate in auto layout, you should implement UIViewController.viewDidLayoutSubviews (or UIView.layoutSubLayers) and set the frame of the layer to the bounds of its super layer's view (the backing layer of a UIView essentially is the CoreGraphics version of that view).
This also means you need to recompute your path in they layout method. if that is expensive then draw your path in unit space and use the transform of the layer to scale it up to the view's dimensions instead.
The solution is to first add the subviews and then create the entire circular progress view at
override func viewDidLayoutSubviews(){
super.viewDidLayoutSubviews()
// Add and position the circular progress bar and its layers here
}

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.

Is it possible to subclass CAAnimation?

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:

Animate bezier straight lines

I want how to animate a bezier straight line like see-saw. Can somebody please suggest. Reference link for animation how does it should look (https://www.youtube.com/watch?v=bjCx128BZK8).
You don't say how you rendered this UIBezierPath, but if you for example added it as a CAShapeLayer you can just apply a CAKeyframeAnimation to it:
// create simple path
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 50, y: view.bounds.size.height / 2))
path.addLineToPoint(CGPoint(x: view.bounds.size.width - 50, y: view.bounds.size.height / 2))
// create `CAShapeLayer` from that path
let layer = CAShapeLayer()
layer.path = path.CGPath
layer.lineWidth = 10
layer.strokeColor = UIColor.blueColor().CGColor
layer.fillColor = UIColor.clearColor().CGColor
view.layer.addSublayer(layer)
// animate it
let animation = CAKeyframeAnimation(keyPath: "transform")
var values = [NSValue]()
for var x: CGFloat = 0.0; x < CGFloat(M_PI * 4); x += CGFloat(M_PI * 4 / 100.0) {
var transform = CATransform3DMakeTranslation(view.frame.size.width/2, view.frame.size.height/2, 0.0)
transform = CATransform3DRotate(transform, sin(x) / 3.0, 0.0, 0.0, 1.0)
transform = CATransform3DTranslate(transform, -view.frame.size.width/2, -view.frame.size.height/2, 0.0)
values.append(NSValue(CATransform3D: transform))
}
animation.duration = 2
animation.values = values
layer.addAnimation(animation, forKey: "transform")
That yields:
Or you could add the CAShapeLayer to a view and then use the block-based UIView animation to rotate the view. The basic idea would be the same.
If you want to do something more sophisticated, you can use CADisplayLink, either updating the path for the CAShapeLayer (as outlined here) or updating the properties used by UIView subclass and then call setNeedsDisplay.

Resources