How to animate object using bezier path? - ios

Here is my code to draw dashed line
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = viewDraw.bounds
shapeLayer.position = CGPoint(x: viewDraw.bounds.width/2, y: viewDraw.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = ColorConstants.ThemeColor.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: viewDraw.bounds, cornerRadius: viewDraw.bounds.size.height/2)
shapeLayer.path = path.cgPath
viewDraw.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 1
shapeLayer.add(animation, forKey: "MyAnimation")
}
Now as per my screenshot I want to animate blue dot to red dot within that dashed line infinite time. When blue dot reaches red dot then it will start again with blue dot points.
Note: This whole screen is dynamic so, Line draw is also dynamic according to screen height
Any help would be appreciated

You are almost there
Now you have to add animation using CAKeyframeAnimation which has property path where you can pass the path where you want to animate
here is working sample code
func drawLine() {
let shapeLayer = CAShapeLayer()
shapeLayer.bounds = self.view.bounds
shapeLayer.position = CGPoint(x: self.view.bounds.width/2, y: self.view.bounds.height/2)
shapeLayer.fillColor = nil
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 3
shapeLayer.lineJoin = CAShapeLayerLineJoin.round // Updated in swift 4.2
shapeLayer.lineDashPattern = [10]
let path = UIBezierPath(roundedRect: self.view.bounds, cornerRadius: self.view.bounds.size.height/2)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let animation1 = CABasicAnimation(keyPath: "strokeEnd")
animation1.fromValue = 0
animation1.duration = 1
shapeLayer.add(animation1, forKey: "MyAnimation")
// Create squareView and add animation
let animation = CAKeyframeAnimation(keyPath: #keyPath(CALayer.position))
animation.duration = 1
animation.repeatCount = MAXFLOAT
animation.path = path.cgPath
let squareView = UIView()
// Don't worry about x,y and width and height it will not affect the animation
squareView.frame = CGRect(x: 0, y: 0, width: 50, height: 50)
squareView.backgroundColor = .lightGray
view.addSubview(squareView)
// You can also pass any unique string value for key
squareView.layer.add(animation, forKey: nil)
}
You can see this tutorial for more help http://mathewsanders.com/animations-in-swift-part-two/

Related

How to make UIBezierPath to start from mid of view (currently it is starting from left corner side of view)

I am using following code to create roundview with progress bar on it layer but progress bar always starting from left top corner side and I want to make it start from top mid of view
func buildRoundView1(roundView: UIView, total: Int, borderColor: UIColor, shapeLayer: CAShapeLayer){
let trackLayer = CAShapeLayer()
let rect = CGRect(x: 0, y: 0, width: roundView.frame.size.width, height: roundView.frame.size.height)
let circularPath = UIBezierPath(roundedRect: rect, cornerRadius: 10)
trackLayer.path = circularPath.cgPath
trackLayer.lineWidth = 4
trackLayer.fillColor = UIColor.clear.cgColor
trackLayer.lineCap = CAShapeLayerLineCap.round
roundView.layer.addSublayer(trackLayer)
shapeLayer.path = circularPath.cgPath
shapeLayer.strokeColor = borderColor.cgColor
shapeLayer.lineWidth = 4
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = CAShapeLayerLineCap.round
shapeLayer.strokeEnd = 0
roundView.layer.addSublayer(shapeLayer)
let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
let startValue = Double(10800-total).normalize(min: 0.0, max: 10800.0)
basicAnimation.fromValue = 1 - startValue
basicAnimation.toValue = 1
basicAnimation.duration = CFTimeInterval(total)
basicAnimation.fillMode = CAMediaTimingFillMode.backwards
basicAnimation.isRemovedOnCompletion = true
shapeLayer.add(basicAnimation, forKey: "urSoBasic")
}
Some ideas:
Make your own geometry instead of using UIBezierPath(roundedRect: rect, cornerRadius: 10) construct it with move to point, add line to point, add curve to point etc etc
Rotate the path you got from UIBezierPath(roundedRect: rect, cornerRadius: 10) by 90 degrees (translate its center to origin, rotate it, translate it back)
Make the bezier path with height and width swapped and rotate shapeLayer by applying a transform to it

image disappears after keyframe animation

I have an animation where a line is drawn and then with that another image moves. The animation works good but as the animation completes the image disappears from the view. The image should stay after completion of the animation. My code for the animation:
private func addPlaneAnimation() {
let image = UIImageView(frame: CGRect(x: -30, y: view.height*0.8, width: 30, height: 30))
image.image = R.image.plane()
view.addSubview(image)
let path = UIBezierPath()
path.move(to: image.center)
path.addLine(to: CGPoint(x: (view.width*0.85), y: (view.height*0.40)))
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = R.color.Purple()?.cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = path.cgPath
view.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 4
animation.beginTime = CACurrentMediaTime() + 0.2
animation.isRemovedOnCompletion = false
let pathAnimation = CAKeyframeAnimation(keyPath: "position")
pathAnimation.path = path.cgPath
pathAnimation.duration = 4
pathAnimation.autoreverses = false
pathAnimation.isRemovedOnCompletion = false
shapeLayer.add(animation, forKey: "MyAnimation")
image.layer.add(pathAnimation, forKey: "position")
view.bringSubviewToFront(cardBarcode)
}
Can someone please help me here?
Just add two more properties.
For strokeEnd animation
animation.fillMode = CAMediaTimingFillMode.backwards
and for pathAnimation
pathAnimation.fillMode = CAMediaTimingFillMode.forwards

Center of CAGradientLayer returns wrong position on rotate animation

I have a circle like in Instagram profile image which has story. I want to have an affect like circle is spinning. For that, I used CABasicAnimation. It is spinning but not in center.
As I searched, I need to give bounty for shapeLayer but when I do that, It doesn't placed where It needs to be.
How can I achieve animation like in Instagram story circle (like circle is spinning)?
EDIT I also try to add "colors" animation but because It works like It is in square, I can't get the desired result.
func addCircle() {
let circularPath = UIBezierPath(arcCenter: CGPoint(x: self.bounds.midX, y: self.bounds.midY), radius: self.bounds.width / 2, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.lineWidth = 10
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineCap = kCALineCapRound
shapeLayer.strokeEnd = 1.0
gradient.frame = circularPath.bounds
gradient.colors = [UIColor.blue.cgColor, UIColor.red.cgColor]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 0)
shapeLayer.path = circularPath.cgPath
gradient.mask = shapeLayer
self.layer.addSublayer(gradient)
}
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc private func handleTap() {
print("Attempting to animate stroke")
shapeLayer.add(rotationAnimation, forKey: "urSoBasic2")
}
If spinning, why not spin gradient
let rotationAnimation: CAAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.fromValue = 0
animation.toValue = Float.pi * 2
animation.duration = 4
animation.repeatCount = MAXFLOAT
return animation
}()
#objc func handleTap() {
print("Attempting to animate stroke")
gradient.add(rotationAnimation, forKey: "urSoBasic2")
}
As to off center problem. The correct frame for gradient should be like this :
gradient.frame = self.bounds
To verify the center, you may add a background for the view:
self.background = UIColor.black.
The reason is width and height of the view is not set well due to the constraints. Try to set the correct bounds of the view, so when you add the circularPath, it's center in the view.

Circular progress in swift

I am trying to make a simple circular progress bar in swift. What I have failed to do so far is, the progress should start at the top of the circle (at the moment it is starting at the 90 degree point). I would also like to have a circular line under the progress line that should be thicker than the progress line and have a different color as well..
Can anyone put me in the right direction towards my wishes?
Here is my function:
func animateView() {
let circle = viewProgress // viewProgress is a UIView
var progressCircle = CAShapeLayer()
let circlePath = UIBezierPath(ovalInRect: circle.bounds)
progressCircle = CAShapeLayer ()
progressCircle.path = circlePath.CGPath
progressCircle.strokeColor = UIColor.greenColor().CGColor
progressCircle.fillColor = UIColor.clearColor().CGColor
progressCircle.lineWidth = 5.0
circle.layer.addSublayer(progressCircle)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.toValue = 1.5
animation.duration = 1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
progressCircle.addAnimation(animation, forKey: "ani")
}
Igor this is how it looks:
For Swift 4
Declare two CAShapeLayers. One for actual circle and another for progress Layer!
func createCircularPath() {
let circleLayer = CAShapeLayer()
let progressLayer = CAShapeLayer()
let center = self.view.center
let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -.pi / 2, endAngle: 2 * .pi, clockwise: true)
circleLayer.path = circularPath.cgPath
circleLayer.fillColor = UIColor.clear.cgColor
circleLayer.strokeColor = UIColor.lightGray.cgColor
circleLayer.lineCap = .round
circleLayer.lineWidth = 20.0 //for thicker circle compared to progressLayer
progressLayer.path = circularPath.cgPath
progressLayer.fillColor = UIColor.clear.cgColor
progressLayer.strokeColor = UIColor.blue.cgColor
progressLayer.lineCap = .round
progressLayer.lineWidth = 10.0
progressLayer.strokeEnd = 0
view.addSublayer(circleLayer)
view.addSublayer(progressLayer)
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.toValue = 1
progressAnimation.fillMode = .forwards
progressAnimation.isRemovedOnCompletion = false
progressLayer.add(progressAnimation, forKey: "progressAnim")
}
Happy Coding!
Hope this answer helps :]

Substract CALayer from another, blend?

On the following image, the blue circle is only for debug purpose. My goal is that every layer behind the blue circle should be transparent.I only want to keep visible what's outside the blue circle.
Here is the code written in swift :
let croissantView = UIView(frame: CGRectMake(100, 100, 100, 100))
croissantView.backgroundColor = UIColor.clearColor()
self.view.addSubview(croissantView)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
croissantView.layer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
croissantView.layer.addAnimation(animRotate, forKey: "animRotate")
let circle = UIBezierPath(arcCenter: croissantView.center, radius: 75, startAngle: 0, endAngle: 360 * CGFloat(M_PI) / 180, clockwise: true);
let circleLayer = CAShapeLayer()
circleLayer.path = circle.CGPath
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
self.view.layer.addSublayer(circleLayer)
Thoughts
Substract from Layer, don't know if feasible
Blend, don't know how to do it with CALayer
Thanks !! :)
There are three things I would suggest.
Change your croissant view to a layer (not sure this was necessary, but this is what I did)
Create a CGPath for your circle shape that can have its mask inverted. You do this by wrapping an outer rect around your circle so that the path looks like this:
Set your fill rule on your circle shape layer to even-odd:
circleLayer.fillRule = kCAFillRuleEvenOdd
This is what it looks like:
I adapted your code to attempt it and this is what I came up with. Your mileage may vary:
func applyMask() {
let mainLayer = CALayer()
mainLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 100.0, height: 100.0)
mainLayer.position = CGPoint(x: 200.0, y: 200.0)
// Set the color for debugging
mainLayer.backgroundColor = UIColor.orangeColor().CGColor
self.view.layer.addSublayer(mainLayer)
let radius = 50 as CGFloat
let width = 50 as CGFloat
let origin = CGPointMake(0, 100)
let end = CGPointMake(100,100)
let highAmplitude = CGPointMake(50,180)
let lowAmplitude = CGPointMake(50,150)
let quad = UIBezierPath()
quad.moveToPoint(origin)
quad.addQuadCurveToPoint(end, controlPoint: highAmplitude)
quad.closePath()
let quad2 = UIBezierPath()
quad2.moveToPoint(origin)
quad2.addQuadCurveToPoint(end, controlPoint: lowAmplitude)
quad2.closePath()
let layer = CAShapeLayer()
layer.path = quad.CGPath
layer.strokeColor = UIColor.greenColor().CGColor
layer.fillColor = UIColor.blackColor().CGColor
mainLayer.addSublayer(layer)
let anim = CABasicAnimation(keyPath: "path")
anim.duration = 3
anim.repeatCount = 20
anim.fromValue = quad.CGPath
anim.toValue = quad2.CGPath
anim.autoreverses = true
layer.addAnimation(anim, forKey: "animQuad")
let animRotate = CABasicAnimation(keyPath: "transform.rotation")
animRotate.duration = 5
animRotate.repeatCount = 20
animRotate.fromValue = 0
animRotate.toValue = 360 * CGFloat(M_PI) / 180
mainLayer.addAnimation(animRotate, forKey: "animRotate")
// Build a Circle CGPath
var circlePath = CGPathCreateMutable()
CGPathAddEllipseInRect(circlePath, nil, CGRectInset(mainLayer.bounds, -20.0, -20.0))
// Invert the mask by adding a bounding rectangle
CGPathAddRect(circlePath, nil, CGRectInset(mainLayer.bounds, -100.0, -100.0))
CGPathCloseSubpath(circlePath)
let circleLayer = CAShapeLayer()
circleLayer.fillColor = UIColor.blueColor().CGColor
circleLayer.opacity = 0.6
// Use the even odd fill rule so that our path gets inverted
circleLayer.fillRule = kCAFillRuleEvenOdd
circleLayer.path = circlePath
mainLayer.mask = circleLayer
mainLayer.addSublayer(layer)
}
Here's the project where I tried it out: https://github.com/perlmunger/SwiftVertedMask
Hope that helps.

Resources