Swift: Animate position of a path on a CAShapeLayer - swift

I have a path and a shape Layer on that I want to draw a line
let path = UIBezierPath()
path.move(to: CGPoint(x: xValue,y: yValue))
path.addLine(to: CGPoint(x: xValue, y: yValue + 300))
// create shape layer for that path
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 3
shapeLayer.path = path.cgPath
But I am also animating the drawing of the line
self.chartView.layer.addSublayer(shapeLayer)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 0.6
shapeLayer.add(animation, forKey: "MyAnimation")
Now to my question:
I also want to animate the position of my path at the meanwhile. So at the moment my line is on the x-Positon of "xValue". I want to move it animated to "xValue - 40.0". I tried it on 2 ways which both didnt work.
use UIView.animate. Problem: The x Position is changing but without an animation:
UIView.animate(withDuration: 3.0) {
var highlightFrame = shapeLayer.frame
highlightFrame.origin.x = (highlightFrame.origin.x) - 40
shapeLayer.frame = highlightFrame
}
add a animation on the shape layer. Nothing happens here
let toPoint:CGPoint = CGPoint(x: -40.0, y: 0.0)
let fromPoint:CGPoint = CGPoint.zero
let movement = CABasicAnimation(keyPath: "movement")
movement.isAdditive = true
movement.fromValue = NSValue(cgPoint: fromPoint)
movement.toValue = NSValue(cgPoint: toPoint)
movement.duration = 0.3
shapeLayer.add(movement, forKey: "move")
Anyone has another solution? Thanks for your help.

let toPoint:CGPoint = CGPoint(x: -40.0, y: 0.0)
let fromPoint:CGPoint = CGPoint.zero
let movement = CABasicAnimation(keyPath: "position") // it should be "position" not "movement"
movement.isAdditive = true
movement.fromValue = NSValue(cgPoint: fromPoint)
movement.toValue = NSValue(cgPoint: toPoint)
movement.duration = 0.3
shapeLayer.add(movement, forKey: "move")

Related

Swift Animate Mask Transition

How can you animate a mask moving down X pixels?
I have a circular mask applied to a black view that creates a spotlight effect. I want to animate the mask transitioning down 100 pixels, but using UIView.animate { mask.transform = CATransform3DTranslate(.init(), 0, 100, 0) } instantly moves it instead of animating the transition.
Here's the mask code:
private lazy var mask: CAShapeLayer = {
let mask = CAShapeLayer()
let path = CGMutablePath()
path.addArc(center: CGPoint(x: 0, y: 0), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
mask.backgroundColor = UIColor.black.cgColor
mask.path = path
mask.fillRule = .evenOdd
return mask
}()
overlayView.layer.mask = mask
How would you animate this moving down 100 pixels?
To get the spotlight effect, you'll want to create paths that modify the arc shape and animate between them.
func spotlightPath(arcCenter: CGPoint) -> CGPath {
let path = CGMutablePath()
// move the spotlight
path.addArc(center: CGPoint(x: arcCenter.x, y: arcCenter.y), radius: 40, startAngle: 0, endAngle: 2.0 * .pi, clockwise: false)
path.addRect(CGRect(origin: .zero, size: view.frame.size))
return path
}
let startPath = spotlightPath(arcCenter: .zero)
And then later
let endPath = spotlightPath(arcCenter: CGPoint(x: 40, y: 100))
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 1
animation.isRemovedOnCompletion = false
animation.fillMode = .forwards
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
animation.toValue = endPath
mask.add(animation, forKey: "path")
Try this Core animation block. CALayer animation is not supported by UIView animation block. To animate mask property we use the path key.
let anim = CABasicAnimation(keyPath: "path")
anim.toValue = createPath(center: .init(x: 100, y: 100))
anim.duration = 1
anim.fillMode = .forwards
anim.timingFunction = CAMediaTimingFunction.init(name: .easeInEaseOut)
anim.isRemovedOnCompletion = false
mask.add(anim, forKey: anim.keyPath)

How to launch gradient animation on button tapped?

I'm creating an IOS application. I have 5 buttons in star shapes created with UIBezierPath. And I want fill them when I tapped specific star. For ex. if I tapped 3-rd star then gradient will fill 3 first stars etc.
I have already created view with 5 stars. In the draw view method I have added code for animating gradient and it works fine only when view is launched.
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
UIColor.black.setStroke()
path.lineWidth = 2
let numberOfPoints = 5
var angle: CGFloat = CGFloat.pi / 2
let angleOfIncrement = CGFloat.pi*2/CGFloat(numberOfPoints)
let radius: CGFloat = rect.origin.x + rect.width/2
let offset = CGPoint(x: rect.origin.x + rect.width/2, y: rect.origin.y + rect.height/2)
let midleRadius = radius*0.45
var firstStar = true
for _ in 1...numberOfPoints {
let firstPoint = getPointLocation(angle: angle, radius: midleRadius, offset: offset)
let midlePoint = getPointLocation(angle: angle+angleOfIncrement/2, radius: radius, offset: offset)
let secondPoint = getPointLocation(angle: angle+angleOfIncrement, radius: midleRadius, offset: offset)
if firstStar {
firstStar = false
path.move(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}else {
path.addLine(to: firstPoint)
path.addLine(to: midlePoint)
path.addLine(to: secondPoint)
}
angle += angleOfIncrement
}
path.stroke()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
let gradient = CAGradientLayer(layer: layer)
gradient.colors = [UIColor.black.cgColor, UIColor.white.cgColor]
gradient.locations = [1]
gradient.startPoint = CGPoint(x: 0, y: 1)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.frame = path.bounds
self.layer.addSublayer(gradient)
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
self.layer.mask = shapeLayer
}
I expect that when I tapped 3-rd star then gradient will fill first 3 stars.
You have to separate your drawing code from your animation code, and (btw.) you should also (re-)move the layer creation out of draw().
For the separation move
let animation = CABasicAnimation(keyPath: "locations")
animation.fromValue = [0, 0]
animation.toValue = [0, 1.0]
animation.duration = 3
gradient.add(animation, forKey: nil)
into a separate method and call it when needed.
You must also remove the layer creation from draw()because this method is always called if the view needs to be (re-)drawn. This produce a serious memory leak, because for instance every rotation of your iPhone will create a new gradient layer.
You have another issue in your code. Your gradient layer lies over the layer of the view. That cannot work. You may make the gradient layer to the view's layer, and work with mask, to cut the stars out of the gradient.

How do I animate two views simultaneously in swift programmatically?

In another question, how to animate was answered:
Draw line animated
I then tried to add a view to animate simultaneously. I want to create two animated paths that meet at the same CGPoint but have different paths to get there. I want both paths to start at the same time and end animation at the same time. My current approach is to create a second UIBezierPath called path2, along with a second CAShapeLayer called shapeLayer2, but the second path is just overriding the first. I'm pretty sure both paths need different CAShapeLayers because each path will have different attributes. It seems like I then need to add the CAShapeLayer instance as a view.layer sublayer but it doesn't seem to be working. What should I be doing differently to achieve the desired result?
Here is what ultimately worked.
func animatePath() {
// create whatever path you want
let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 100))
path.addLine(to: CGPoint(x: 200, y: 50))
path.addLine(to: CGPoint(x: 200, y: 240))
// create whatever path you want
let path2 = UIBezierPath()
let bottomStartX = view.frame.maxX - 10
let bottomStartY = view.frame.maxY - 100
path2.move(to: CGPoint(x: bottomStartX, y: bottomStartY))
path2.addLine(to: CGPoint(x: bottomStartX - 100, y: bottomStartY - 100))
path2.addLine(to: CGPoint(x: 200, y: 240))
// create shape layer for that path
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer.strokeColor = #colorLiteral(red: 1, green: 0, blue: 0, alpha: 1).cgColor
shapeLayer.lineWidth = 4
shapeLayer.path = path.cgPath
let shapeLayer2 = CAShapeLayer()
shapeLayer2.fillColor = #colorLiteral(red: 0, green: 0, blue: 0, alpha: 0).cgColor
shapeLayer2.strokeColor = UIColor.blue.cgColor
shapeLayer2.lineWidth = 4
shapeLayer2.path = path.cgPath
// animate it
view.layer.addSublayer(shapeLayer)
view.layer.addSublayer(shapeLayer2)
let animation = CABasicAnimation(keyPath: "strokeEnd")
animation.fromValue = 0
animation.duration = 2
shapeLayer.add(animation, forKey: "MyAnimation")
let animation2 = CABasicAnimation(keyPath: "strokeEnd")
animation2.fromValue = 0
animation2.duration = 2
shapeLayer2.add(animation2, forKey: "MyAnimation")
// save shape layer
self.shapeLayer = shapeLayer
}

How do I flip over a UIBezierPath or cgPath thats animated onto the CAShapeLayer?

I have an array of type [UIBezierPath] that I transform into cgPaths and then animate onto a CAShapeLayer I called shapeLayer. Now for some reason all my paths are upside down, so all the paths get drawn upside down. How can I fix this, I tried a couple of methods but sadly none of them worked... I did however figure out how to scale the path. This is my code to draw the swiftPath, which is a path made up of UIBezierPaths found in the Forms class under the function swiftBirdForm(). Drawing the path is working fine, I just can't figure out how to flip it 180 degrees.
#objc func drawForm() {
var swiftPath = Forms.swiftBirdForm()
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 1
shapeLayer.frame = CGRect(x: -120, y: 120, width: 350, height: 350)
var paths: [UIBezierPath] = swiftPath
guard let path = paths.first else {
return
}
paths.dropFirst()
.forEach {
path.append($0)
}
shapeLayer.transform = CATransform3DMakeScale(0.6, 0.6, 0)
shapeLayer.path = path.cgPath
self.view.layer.addSublayer(shapeLayer)
let strokeEndAnimation = CABasicAnimation(keyPath: "strokeEnd")
strokeEndAnimation.duration = 1.0
strokeEndAnimation.fromValue = 0.0
strokeEndAnimation.toValue = 1.0
shapeLayer.add(strokeEndAnimation, forKey: nil)
}
Using CATransform3D
shapeLayer.transform = CATransform3DMakeScale(1, -1, 1)
Transforming path,
let shapeBounds = shapeLayer.bounds
let mirror = CGAffineTransform(scaleX: 1,
y: -1)
let translate = CGAffineTransform(translationX: 0,
y: shapeBounds.size.height)
let concatenated = mirror.concatenating(translate)
bezierPath.apply(concatenated)
shapeLayer.path = bezierPath.cgPath
Transforming layer,
let shapeFrame = CGRect(x: -120, y: 120, width: 350, height: 350)
let mirrorUpsideDown = CGAffineTransform(scaleX: 1,
y: -1)
shapeLayer.setAffineTransform(mirrorUpsideDown)
shapeLayer.frame = shapeFrame

Efficient resize the CAShapeLayer if the view grows animatable

I have a UIButton with a CAShapeLayer inside it. This is part of the subclass's code:
private func addBeneathFill(){
didAddedBeneathFill = true
let halfBeneathFill = UIBezierPath()
halfBeneathFill.move(to: CGPoint(x: 0, y: frame.height / 2))
halfBeneathFill.addCurve(to: CGPoint(x: frame.width, y: frame.height / 2), controlPoint1: CGPoint(x: frame.width / 10, y: frame.height / 2 + frame.height / 10), controlPoint2: CGPoint(x: frame.width, y: frame.height / 2 + frame.height / 10))
halfBeneathFill.addLine(to: CGPoint(x: frame.width, y: frame.height))
halfBeneathFill.addLine(to: CGPoint(x: 0, y: frame.height))
halfBeneathFill.addLine(to: CGPoint(x: 0, y: frame.height / 2))
let path = halfBeneathFill.cgPath
let shapeLayer = CAShapeLayer()
shapeLayer.path = path
let fillColor = halfBeneathFillColor.cgColor
shapeLayer.fillColor = fillColor
shapeLayer.opacity = 0.3
layer.insertSublayer(shapeLayer, at: 2)
}
It works great as long as the UIButton does not change in size. When it changes in size, the CAShapeLayer just stays in it's original position. Of course I can run the method again when the UIButton's frame has been changed, but in an animation it looks awful.
How can I correctly animate the CAShapeLayer's size to be always the current size of the UIButton?
You need to recalculate and animate your path on button frame change.
private func animateShapeLayer() {
let animation = CABasicAnimation(keyPath: "path")
animation.fromValue = getBezierPath(forFrame: oldFrame)
animation.toValue = getBezierPath(forFrame: newFrame)
animation.duration = 1.0
animation.fillMode = kCAFillModeForwards
animation.isRemovedOnCompletion = false
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
shapeLayer.add(animation, forKey: "path")
}
I've tested it, here you have a link to the playground gist and achieved result: