Using UIViewPropertyAnimator to drive animation along curved path? - swift

I have an interruptible transition that is driven by UIViewPropertyAnimator and I would like to animate a view along a non linear path to its destination.
Doing some research I've found that the way to do this is by using:
CAKeyframeAnimation. However, I'm having issue coordinating the animations also I'm having issue with the CAKeyframeAnimation fill mode.
It appears to be reverse even though I've set it to forwards.
Below is the bezier path:
let bezierPath = self.generateCurve(from: CGPoint(x: self.itemStartFrame.midX, y: self.itemStartFrame.midY), to: CGPoint(x: self.itemEndFrame.midX, y: self.itemEndFrame.midY), bendFactor: 1, thickness: 1)
func generateCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {
let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
let normalNormalized: CGPoint = {
let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
guard normalSize > 0.0 else { return .zero }
return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
}()
let path = UIBezierPath()
path.move(to: from)
let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)
path.addQuadCurve(to: to, controlPoint: closeControlPoint)
path.addQuadCurve(to: from, controlPoint: farControlPoint)
path.close()
return path
}
Path visualization:
let shapeLayer = CAShapeLayer()
shapeLayer.path = bezierPath.cgPath
shapeLayer.strokeColor = UIColor.red.cgColor
shapeLayer.lineWidth = 2
containerView.layer.addSublayer(shapeLayer)
Here's the CAKeyframeAnimation that resides inside of the UIViewPropertyAnimator
let curvedPathAnimation = CAKeyframeAnimation(keyPath: "position")
curvedPathAnimation.path = bezierPath.cgPath
curvedPathAnimation.fillMode = kCAFillModeForwards
curvedPathAnimation.calculationMode = kCAAnimationPaced
curvedPathAnimation.duration = animationDuration
self.attatchmentView.layer.add(curvedPathAnimation, forKey: nil)
So far this animation along the correct path, however the animation reverse back to it's starting position and is not in sync with the property animator.
Any suggestions?

Related

Rotating CAShapeLayer with user touch & drag event and CAShapeLayer's center point is nailed

Here is my demo:
As, you can see, the tip of the line is not rotated. This what I want to achieve:
How I draw the original line: When I tap on the screen, I got a CGPoint and draw everything base on that
func drawWholeRuler(originalPoint:CGPoint){ //Draw a whole ruler with every components
endPoint = CGPoint(x: originalPoint.x - 70, y: originalPoint.y)
startPoint = CGPoint(x: originalPoint.x + 70, y: originalPoint.y)
dotStartPointX = CGPoint(x: startPoint!.x, y: startPoint!.y - dotLineSize)
dotStartPointY = CGPoint(x: startPoint!.x, y: startPoint!.y + dotLineSize)
dotEndPointX = CGPoint(x: endPoint!.x, y: endPoint!.y - dotLineSize)
dotEndPointY = CGPoint(x: endPoint!.x, y: endPoint!.y + dotLineSize)
let path = drawLineFromPoint(start: startPoint!, toPoint: endPoint!, ofColor: fillColor, inView: self)
let dotStart = drawLineFromPoint(start: dotStartPointX!, toPoint: dotStartPointY!, ofColor: fillColor, inView: self)
let dotEnd = drawLineFromPoint(start: dotEndPointX!, toPoint: dotEndPointY!, ofColor: fillColor, inView: self)
let circleStart = drawCircle(point: startPoint!)
let circleEnd = drawCircle(point: endPoint!)
let newPath:bezierPathStruct = bezierPathStruct(startPoint: startPoint!, endPoint: endPoint!, dotStartPointX: dotStartPointX!, dotStartPointY: dotStartPointY!, dotEndPointX: dotEndPointX!, dotEndPointY: dotEndPointY!, path: path, dotStart: dotStart, dotEnd: dotEnd, circleStart: circleStart, circleEnd:circleEnd)
bezierPathArray.append(newPath)
}
Now is how I handle longPress and extend the line, which is actually just delete the tapped line and redraw everything separately (middle line, 2 vertical lines, 2 bubbles) when touch moves
#objc func handleLongPress(recognizer: UIGestureRecognizer) {
var startPointOfTouchedRuler:CGPoint = .zero
let zeroPoint:CGPoint = .zero
let currentPanPoint = longTapRecognizer.location(in: self)
// print("here",currentPanPoint)
if let sublayers = self.layer.sublayers as? [CAShapeLayer]{ //get all CAShape and stored as an array
for layer in sublayers{ // go through each CAShape
if let path = layer.path, path.contains(currentPanPoint) { // if there is a path at that point then return, else create a path
startPointOfTouchedRuler = detectWhichRuler(layer: layer)
if startPointOfTouchedRuler != zeroPoint{
// drawCircle(point: startPointOfTouchedRuler)
break
}else{
}
}
}
}
let linePath = UIBezierPath()
var circlePath = UIBezierPath()
var circlePath2 = UIBezierPath()
let verticalLinePath1 = UIBezierPath()
let verticalLinePath2 = UIBezierPath()
switch longTapRecognizer.state {
case .began:
tapGestureStartPoint = startPointOfTouchedRuler
if tapGestureStartPoint == zeroPoint {return}
self.layer.addSublayer(lineShape)
self.layer.addSublayer(shapeLayer1)
self.layer.addSublayer(shapeLayer2)
self.layer.addSublayer(verticalLineShape)
self.layer.addSublayer(verticalLineShape2)
shapeLayer1.anchorPoint = startPointOfTouchedRuler
verticalLinePath1.move(to: CGPoint(x: startPointOfTouchedRuler.x, y: startPointOfTouchedRuler.y - dotLineSize ))
verticalLinePath1.addLine(to: CGPoint(x: startPointOfTouchedRuler.x , y: startPointOfTouchedRuler.y + dotLineSize))
verticalLinePath2.move(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y - dotLineSize))
verticalLinePath2.addLine(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y + dotLineSize))
verticalLineShape.path = verticalLinePath1.cgPath
verticalLineShape2.path = verticalLinePath2.cgPath
linePath.move(to: tapGestureStartPoint)
linePath.addLine(to: currentPanPoint)
circlePath = UIBezierPath(arcCenter: currentPanPoint, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
circlePath2 = UIBezierPath(arcCenter: startPointOfTouchedRuler, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
shapeLayer2.path = circlePath2.cgPath
shapeLayer1.path = circlePath.cgPath
lineShape.path = linePath.cgPath
case .changed:
//
verticalLinePath2.move(to: CGPoint(x: currentPanPoint.x , y: currentPanPoint.y - dotLineSize))
verticalLinePath2.addLine(to: CGPoint(x: currentPanPoint.x, y: currentPanPoint.y + dotLineSize))
verticalLineShape2.path = verticalLinePath2.cgPath
linePath.move(to: tapGestureStartPoint)
linePath.addLine(to: currentPanPoint)
circlePath = UIBezierPath(arcCenter: currentPanPoint, radius: 15.0, startAngle: CGFloat(0), endAngle: CGFloat(Double.pi * 2.0), clockwise: true)
shapeLayer1.path = circlePath.cgPath
circlePath.move(to: tapGestureStartPoint)
lineShape.path = linePath.cgPath
case .ended:
verticalLineShape.path = nil
verticalLineShape2.path = nil
verticalLineShape2.removeFromSuperlayer()
verticalLineShape.removeFromSuperlayer()
shapeLayer2.path = nil
shapeLayer2.removeFromSuperlayer()
lineShape.path = nil
shapeLayer1.path = nil
lineShape.removeFromSuperlayer()
shapeLayer1.removeFromSuperlayer()
if tapGestureStartPoint == zeroPoint {return}
extendALine(startPoint: tapGestureStartPoint, currentPoint: currentPanPoint)
shouldDeleteRuler = true
default: print("default")
break
}
}
Now what I want is to rotate 2 lines at the edge when user hold and move finger. I tried CATransform3DRotate but it doesn't change anything.
I also tried
verticalLineShape.anchorPoint = startPointOfTouchedRuler
let degrees = 90.0
let radians = CGFloat(degrees * .pi / 180)
verticalLineShape.transform = CATransform3DMakeRotation(radians, 0.0, 0.0, 1.0)
verticalLineShape.anchorPoint = startPointOfTouchedRuler
But verticalLineShape disappeared instead of rotating.
So how is the best way to approach this?
I figured out, instead of rotating CAShapeLayer, I just need too rotate UIBezierPath before verticalLineShape.path = verticalLinePath1.cgPath
What I did:
extension UIBezierPath {
func rotate(path:UIBezierPath, degree:CGFloat){
let bounds:CGRect = path.cgPath.boundingBox
let center:CGPoint = CGPoint(x: bounds.midX, y: bounds.midY)
let radians:CGFloat = (degree/180 * .pi)
var transform:CGAffineTransform = CGAffineTransform.identity
transform = transform.translatedBy(x: center.x, y: center.y);
transform = transform.rotated(by: radians);
transform = transform.translatedBy(x: -center.x, y: -center.y);
path.apply(transform)
}
}
use
verticalLinePath1.rotate(path: verticalLinePath1, degree: 30)

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.

UIView subview being placed in unexpected position (Swift 4)

I am attempting to add 4 UIView subviews to a UIImageView. These subviews are to act as nodes where a user can tap them and connect to other nodes. For example, they should look
like this. Instead, they are looking like this.
My code for calculating the node positions is as follows:
func initializeConnectionNodes() {
let imageCenter = self.imageView.center
let xOffset = self.imageView.bounds.width/2 //distance from origin x-wise
let yOffset = self.imageView.bounds.height/2 //distance from origin y-wise
self.leftConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: imageCenter.x - xOffset, y: imageCenter.y))
self.rightConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: imageCenter.x + xOffset, y: imageCenter.y))
self.topConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: imageCenter.x, y: imageCenter.y + yOffset))
self.bottomConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: imageCenter.x, y: imageCenter.y - yOffset))
self.imageView.addSubview(self.leftConnectionNode!)
self.imageView.addSubview(self.rightConnectionNode!)
self.imageView.addSubview(self.topConnectionNode!)
self.imageView.addSubview(self.bottomConnectionNode!)
}
My code for initialization of the UIView class is as follows:
class ConnectionNodeView: UIView {
var connectionPoint: CGPoint
fileprivate var circleLayer: CAShapeLayer?
init(connectionPoint: CGPoint) {
self.connectionPoint = connectionPoint
super.init(frame: CGRect(x: connectionPoint.x, y: connectionPoint.y, width: 0, height: 0))
let circlePath = UIBezierPath(arcCenter: connectionPoint, radius: CGFloat(8), startAngle: CGFloat(0), endAngle:CGFloat(Double.pi * 2), clockwise: true)
self.circleLayer = CAShapeLayer()
self.circleLayer?.path = circlePath.cgPath
self.circleLayer?.fillColor = UIColor.yellow.cgColor
self.circleLayer?.strokeColor = UIColor.yellow.cgColor
self.circleLayer?.lineWidth = 3.0
self.layer.addSublayer(circleLayer!)
}
It is interesting to note that if I just add the CAShapeLayer as a sublayer to my UIImageView, it looks like it should. However, I need to implement it as a UIView so that I can use gesture recognizers easily. I found a dirty way of fixing it by dividing the coordinates by 100 in the initializer like this:
super.init(frame: CGRect(x: connectionPoint.x/100, y: connectionPoint.y/100, width: 0, height: 0))
However, I would rather do it correctly. What am I missing here? Thank you for your help.
You’re adding the views to your image view, but the imageCenter point is given according to the superview of the image view.
Replace the beginning of the initializeConnectionNodes function with the following:
let xCenter = imageView.bounds.width / 2
let yCenter = imageView.bounds.height / 2
leftConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: 0, y: yCenter))
rightConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: imageView.bounds.width, y: yCenter))
topConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: xCenter, y: 0))
bottomConnectionNode = ConnectionNodeView(connectionPoint: CGPoint(x: xCenter, y: imageView.bounds.height))
Also, you should replace the arc center of circlePath in your ConnectionNodeView subclass with CGPoint.zero, since it works with the coordinate system of the node view itself:
let circlePath = UIBezierPath(arcCenter: .zero, radius: 8, startAngle: 0, endAngle:CGFloat(Double.pi * 2), clockwise: true)

CATransform3DRotate applied to layer doesnt work

Could you please advice why CATransform3DRotate doesnt work for layer in my case, its blue layer on the image.
I have custom view, where I draw nature view, I want to animate changing the moon phase that will be done inside the white circle layer as its mask. I suppose that it is good idea to apply here 3DRotation, but for some reason it doesn't work even without animation, could be please advice what I am doing wrong?
func drawMoonPhase(inRect rect:CGRect, inContext context: CGContext) {
let moonShape = UIBezierPath(ovalIn: rect)
moonShape.lineWidth = 4.0
UIColor.white.setStroke()
moonShape.stroke()
moonShape.close()
let moonLayer = CAShapeLayer()
moonLayer.path = moonShape.cgPath
moonLayer.opacity = 0
self.layer.addSublayer(moonLayer)
let circlePath = UIBezierPath(ovalIn: rect)
UIColor.blue.setFill()
circlePath.fill()
circlePath.close()
let circleShape = CAShapeLayer()
circleShape.path = circlePath.cgPath
circleShape.opacity = 0
var transform = CATransform3DIdentity
transform.m34 = -1 / 500.0
transform = CATransform3DRotate(transform, (CGFloat(Double.pi * 0.3)), 0, 1, 0)
circleShape.transform = transform
moonLayer.mask = circleShape
}
Thanks in advance
EDIT:
Maybe I want clear , the effect i want is the below:
The transform occurs around the layer's anchor point that by default is in the center. Therefore what it is happening there is that the shape rotates around itself causing no visible result. :)
what you should do in this layout of layer is to use cos and sin math functions in order to determine the x and y position of your moon.
Let me know if you need more insights I will be happy to help.
also, please note that you don't need 2 shapeLayers in order to have the blue moon with the white stroke. CAShapeLayer has properties for both fill and stroke so you can simplify your code.
Based on the new info here is my new answer:
I was not able to get a nice effect by using the transform, so I decided to write the mask manually. this is the result:
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.black.cgColor
return mask
}
The function above draws a shapelier that can be used as mask for your moonLayer. This layer will be drawnin relation to a progress parameter that you will pass in the function where 1 is full moon and 0 is new moon.
You can put everything together to have the desired effect, and you can extract the path creation code to make a nice animation if you want.
This should answer your question I hope.
To quick test I wrote this playground:
import UIKit
let rect = CGRect(origin: .zero, size: CGSize(width: 200, height: 200))
let view = UIView(frame: rect)
view.backgroundColor = .black
let layer = view.layer
/**
Draw a mask based on the moon phase progress.
- parameter rect: The rect where the mon will be drawn
- parameter progress: The progress of the moon phase. This value must be between 0 and 1
*/
func moonMask(in rect: CGRect, forProgress progress: CGFloat)->CALayer {
let path = CGMutablePath()
let center = CGPoint(x: rect.midX, y: rect.midY)
path.move(to: CGPoint(x: rect.midX, y: 0))
let relativeProgress = (max(min(progress, 1), 0) - 0.5) * 2
let radius = rect.width/2
let tgX = rect.midX+(relativeProgress * (radius*4/3))
path.addCurve(to: CGPoint(x: rect.midX, y: rect.maxY), control1: CGPoint(x: tgX, y: 0), control2: CGPoint(x: tgX, y: rect.maxY))
path.addArc(center: center, radius: rect.width/2, startAngle: .pi/2, endAngle: .pi*3/2, clockwise: false)
//path.closeSubpath()
let mask = CAShapeLayer()
mask.path = path
mask.fillColor = UIColor.white.cgColor
return mask
}
let moonLayer = CAShapeLayer()
moonLayer.path = UIBezierPath(ovalIn: rect).cgPath
moonLayer.fillColor = UIColor.clear.cgColor
moonLayer.strokeColor = UIColor.white.cgColor
moonLayer.lineWidth = 2
moonLayer.shadowColor = UIColor.white.cgColor
moonLayer.shadowOpacity = 1
moonLayer.shadowRadius = 10
moonLayer.shadowPath = moonLayer.path
moonLayer.shadowOffset = .zero
layer.addSublayer(moonLayer)
let moonPhase = moonMask(in: rect, forProgress: 0.3)
moonPhase.shadowColor = UIColor.white.cgColor
moonPhase.shadowOpacity = 1
moonPhase.shadowRadius = 10
moonPhase.shadowPath = moonLayer.path
moonPhase.shadowOffset = .zero
layer.addSublayer(moonPhase)
view

Swift draw shadow to a uibezier path

I have a strange question. Even though I did read a lot of tutorials on how to do this, the final result only shows the bezier line, not any shadow whatsoever. My code is pretty simple :
let borderLine = UIBezierPath()
borderLine.moveToPoint(CGPoint(x:0, y: y! - 1))
borderLine.addLineToPoint(CGPoint(x: x!, y: y! - 1))
borderLine.lineWidth = 2
UIColor.blackColor().setStroke()
borderLine.stroke()
let shadowLayer = CAShapeLayer()
shadowLayer.shadowOpacity = 1
shadowLayer.shadowOffset = CGSize(width: 0,height: 1)
shadowLayer.shadowColor = UIColor.redColor().CGColor
shadowLayer.shadowRadius = 1
shadowLayer.masksToBounds = false
shadowLayer.shadowPath = borderLine.CGPath
self.layer.addSublayer(shadowLayer)
What am I doing wrong as I dont seem to see anything wrong but of course I am wrong since no shadow appears. The function is drawRect, basic UIVIew no extra anything in there, x and y are the width and height of the frame. Many thanks in advance!
I take this example straight from my PaintCode-app. Hope this helps.
//// General Declarations
let context = UIGraphicsGetCurrentContext()
//// Shadow Declarations
let shadow = UIColor.blackColor()
let shadowOffset = CGSizeMake(3.1, 3.1)
let shadowBlurRadius: CGFloat = 5
//// Bezier 2 Drawing
var bezier2Path = UIBezierPath()
bezier2Path.moveToPoint(CGPointMake(30.5, 90.5))
bezier2Path.addLineToPoint(CGPointMake(115.5, 90.5))
CGContextSaveGState(context)
CGContextSetShadowWithColor(context, shadowOffset, shadowBlurRadius, (shadow as UIColor).CGColor)
UIColor.blackColor().setStroke()
bezier2Path.lineWidth = 1
bezier2Path.stroke()
CGContextRestoreGState(context)
I prefer the way to add a shadow-sublayer. You can easily use the following function (Swift 3.0):
func createShadowLayer() -> CALayer {
let shadowLayer = CALayer()
shadowLayer.shadowColor = UIColor.red.cgColor
shadowLayer.shadowOffset = CGSize.zero
shadowLayer.shadowRadius = 5.0
shadowLayer.shadowOpacity = 0.8
shadowLayer.backgroundColor = UIColor.clear.cgColor
return shadowLayer
}
And finally, you just add it to your line path (CAShapeLayer):
let line = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 50, y: 100))
path.addLine(to: CGPoint(x: 100, y: 50))
line.path = path.cgPath
line.strokeColor = UIColor.blue.cgColor
line.fillColor = UIColor.clear.cgColor
line.lineWidth = 2.0
view.layer.addSublayer(line)
let shadowSubLayer = createShadowLayer()
shadowSubLayer.insertSublayer(line, at: 0)
view.layer.addSublayer(shadowSubLayer)
I am using the shadow properties of my shape layer to add shadow to it. The best part of this approach is that I don't have to provide a path explicitly. The shadow follows the path of the layer. I am also animating the layer by changing path. In that case too the shadow animates seamlessly without a single line of code.
Here is what I am doing (Swift 4.2)
shapeLayer.path = curveShapePath(postion: initialPosition)
shapeLayer.strokeColor = UIColor.clear.cgColor
shapeLayer.fillColor = shapeBackgroundColor
self.layer.addSublayer(shapeLayer)
if shadow {
shapeLayer.shadowRadius = 5.0
shapeLayer.shadowColor = UIColor.gray.cgColor
shapeLayer.shadowOpacity = 0.8
}
The curveShapePath method is the one that returns the path and is defined as follows:
func curveShapePath(postion: CGFloat) -> CGPath {
let height: CGFloat = 37.0
let path = UIBezierPath()
path.move(to: CGPoint(x: 0, y: 0)) // start top left
path.addLine(to: CGPoint(x: (postion - height * 2), y: 0)) // the beginning of the trough
// first curve down
path.addCurve(to: CGPoint(x: postion, y: height),
controlPoint1: CGPoint(x: (postion - 30), y: 0), controlPoint2: CGPoint(x: postion - 35, y: height))
// second curve up
path.addCurve(to: CGPoint(x: (postion + height * 2), y: 0),
controlPoint1: CGPoint(x: postion + 35, y: height), controlPoint2: CGPoint(x: (postion + 30), y: 0))
// complete the rect
path.addLine(to: CGPoint(x: self.frame.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
path.addLine(to: CGPoint(x: 0, y: self.frame.height))
path.close()
return path.cgPath
}