Draw Border on Curve Shape Layer - ios

I have already seen this How to get a border on UIBezierPath
I want to draw border on curve
I have drawn curve with following method
func drawCurve(from startPoint: CGPoint, to endPoint: CGPoint, controlPoint: CGPoint) {
var bezierPath = UIBezierPath()
bezierPath.move(to: startPoint)
// bezierPath.addQuadCurve(to: endPoint, controlPoint: CGPoint
bezierPath.addLine(to: controlPoint)
bezierPath.addLine(to: endPoint);
bezierPath = bezierPath.bezierCardinal(withTension: 2.06)
curveSize = bezierPath.bounds
let strokeColor = UIColor.white
if curveLayer != nil {
curveLayer?.removeFromSuperlayer()
curveLayer = nil
}
curveLayer = CAShapeLayer()
curveLayer?.lineWidth = 1.0 / self.zoomScale
curveLayer?.fillColor = UIColor.clear.cgColor
curveLayer?.path = bezierPath.cgPath
curveLayer?.strokeColor = strokeColor.cgColor
viewBase.layer.addSublayer(curveLayer!)
}
I have try to put
curveLayer?.borderWidth = 1.0
curveLayer?.borderColor = UIColor.yellow.cgColor
But it doesn't draw border around (In box )

Try below code-
lineShapeBorder = CAShapeLayer()
lineShapeBorder.zPosition = 0.0
lineShapeBorder.strokeColor = UIColor.blue.cgColor
lineShapeBorder.lineWidth = 25
lineShapeBorder.lineCap = kCALineCapRound
lineShapeBorder.lineJoin = kCALineJoinRound
lineShapeFill = CAShapeLayer()
lineShapeBorder.addSublayer(lineShapeFill)
lineShapeFill.zPosition = 0.0
lineShapeFill.strokeColor = UIColor.green.cgColor
lineShapeFill.lineWidth = 20.0
lineShapeFill.lineCap = kCALineCapRound
lineShapeFill.lineJoin = kCALineJoinRound
// ...
var path = UIBezierPath()
path.move(to: lineStart)
path.addLine(to: lineEnd)
lineShapeFill.path = path.cgPath
lineShapeBorder.path = lineShapeFill.path
Hope it helps!

With the Idea of Abhishek, I have draw Border On Shape with this code
let bazierPath2 = UIBezierPath.init(rect: curveSize)
curveLayerForGingivalLine = CAShapeLayer()
curveLayerForGingivalLine?.zPosition = 0.0
curveLayerForGingivalLine?.strokeColor = UIColor.red.cgColor
curveLayerForGingivalLine?.lineWidth = 1.0 / self.zoomScale
curveLayerForGingivalLine?.fillColor = UIColor.clear.cgColor
let lineShapeBorder = CAShapeLayer()
curveLayerForGingivalLine?.addSublayer(lineShapeBorder)
lineShapeBorder.lineWidth = 1.0 / self.zoomScale
lineShapeBorder.fillColor = UIColor.clear.cgColor
lineShapeBorder.path = bezierPath.cgPath
lineShapeBorder.strokeColor = strokeColor.cgColor
curveLayerForGingivalLine?.path = bazierPath2.cgPath
viewBase.layer.addSublayer(curveLayerForGingivalLine!)

Related

How to animate object using bezier path?

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/

UIView border with several colors with equal sectors

For example, I have a UIView and I need to make border with 3 colors.
I can do like that:
func addDividedColors(colors:[UIColor], width:CGFloat = 1) {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: .zero, size: self.bounds.size)
var colorsArray: [CGColor] = []
var locationsArray: [NSNumber] = []
for (index, color) in colors.enumerated() {
colorsArray.append(color.cgColor)
colorsArray.append(color.cgColor)
locationsArray.append(NSNumber(value: 1.0 / Double(colors.count) * Double(index)))
locationsArray.append(NSNumber(value: 1.0 / Double(colors.count) * Double(index + 1)))
}
gradientLayer.locations = locationsArray
gradientLayer.colors = colorsArray
let shape = CAShapeLayer()
shape.lineWidth = width
shape.path = UIBezierPath(roundedRect: self.bounds.insetBy(dx: width/2, dy: width/2), cornerRadius: self.cornerRadius).cgPath
shape.strokeColor = UIColor.black.cgColor
shape.fillColor = UIColor.clear.cgColor
gradientLayer.mask = shape
self.insertSublayer(gradientLayer, at: 0)
}
but then I'll see border with 4 sectors:
But It should looks like:
Is only way - to create circle with several colours, or it can be achieved by border?
Thanks!
So, I finally did this task. Here is an extension for CALayer:
func addDividedColors(colors: [UIColor], width: CGFloat = 10) {
let circlePath = UIBezierPath(ovalIn: frame)
var segments: [CAShapeLayer] = []
let segmentAngle: CGFloat = 1.0/CGFloat(colors.count)
for index in 0..<colors.count{
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
// start angle is number of segments * the segment angle
circleLayer.strokeStart = segmentAngle * CGFloat(index)
// end angle is the start plus one segment
circleLayer.strokeEnd = circleLayer.strokeStart + segmentAngle
circleLayer.lineWidth = width
circleLayer.strokeColor = colors[index].cgColor
circleLayer.fillColor = UIColor.clear.cgColor
// add the segment to the segments array and to the view
segments.insert(circleLayer, at: index)
addSublayer(segments[index])
}
}
And how it looks like now:

Draw dotted line at bottom of UITextField

I am trying to draw a dotted line at bottom of UITextField, but not got success. Below is what i tried so far. Please guide.
func addDashedBorder() {
let color = UIColor.white.cgColor
let width = CGFloat(2.0)
let shapeLayer:CAShapeLayer = CAShapeLayer()
let frameSize = self.frame.size
let shapeRect = CGRect(x: 0, y: frameSize.height, width: frameSize.width, height: 2.0)
shapeLayer.bounds = shapeRect
shapeLayer.position = CGPoint(x: frameSize.width/2, y: frameSize.height - width)
shapeLayer.fillColor = UIColor.darkGray.cgColor
shapeLayer.strokeColor = color
shapeLayer.lineWidth = 2.0
// shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [6,3]
shapeLayer.path = UIBezierPath(rect: shapeRect).cgPath//UIBezierPath(roundedRect: shapeRect, cornerRadius: 0).cgPath
self.layer.addSublayer(shapeLayer)
}
}
Output getting is:
Expected is:
Finally fixed:
extension UIView {
func addDashedLine(strokeColor: UIColor, lineWidth: CGFloat) {
backgroundColor = .clear
let shapeLayer = CAShapeLayer()
shapeLayer.name = "DashedTopLine"
shapeLayer.bounds = bounds
shapeLayer.position = CGPoint(x: frame.width / 2, y: frame.height * 1.2)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [6, 4]
let path = CGMutablePath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: frame.width, y: 0))
shapeLayer.path = path
layer.addSublayer(shapeLayer)
}
}
Got help from here: [drawing dashed line using CALayer
]1
Try this
Use dot image :
self.textField.layer.borderWidth = 3
self.textField.layer.borderColor = (UIColor(patternImage: UIImage(named: "dot")!)).CGColor
Updated:
Use extension below
mytextfield.addDashedLine(strokeColor:.red,lineWidth:1)
extension UIView {
func addDashedLine(strokeColor: UIColor, lineWidth: CGFloat) {
backgroundColor = .clear
let shapeLayer = CAShapeLayer()
shapeLayer.name = "DashedTopLine"
shapeLayer.bounds = bounds
shapeLayer.position = CGPoint(x:30, y: 40)
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.lineWidth = lineWidth
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineDashPattern = [4, 4]
let path = CGMutablePath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x:500, y: 0))
shapeLayer.path = path
layer.addSublayer(shapeLayer)
}
}

Add gradient to CAShapeLayer

I got this CAShapeLayer drawing a line in a chart for me:
open func generateLayer(path: UIBezierPath) -> CAShapeLayer {
let lineLayer = CAShapeLayer()
lineLayer.lineJoin = lineJoin.CALayerString
lineLayer.lineCap = lineCap.CALayerString
lineLayer.fillColor = UIColor.clear.cgColor
lineLayer.lineWidth = lineWidth
lineLayer.strokeColor = lineColors.first?.cgColor ?? UIColor.white.cgColor
lineLayer.path = path.cgPath
if dashPattern != nil {
lineLayer.lineDashPattern = dashPattern as [NSNumber]?
}
if animDuration > 0 {
lineLayer.strokeEnd = 0.0
let pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
pathAnimation.duration = CFTimeInterval(animDuration)
pathAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)
pathAnimation.fromValue = NSNumber(value: 0 as Float)
pathAnimation.toValue = NSNumber(value: 1 as Float)
pathAnimation.autoreverses = false
pathAnimation.isRemovedOnCompletion = false
pathAnimation.fillMode = kCAFillModeForwards
pathAnimation.beginTime = CACurrentMediaTime() + CFTimeInterval(animDelay)
lineLayer.add(pathAnimation, forKey: "strokeEndAnimation")
} else {
lineLayer.strokeEnd = 1
}
return lineLayer
}
Now I'd like to draw this line with a gradient instead of a single color. This is what I came up with, but unfortunately it does not draw the line for me. Without this added code (lineColors.count == 1) the line is drawn correctly with a single color.
fileprivate func show(path: UIBezierPath) {
let lineLayer = generateLayer(path: path)
layer.addSublayer(lineLayer)
if lineColors.count > 1 {
let gradientLayer = CAGradientLayer()
gradientLayer.startPoint = CGPoint(x: 0.0, y: 0.5)
gradientLayer.endPoint = CGPoint(x: 1.0, y: 0.5)
gradientLayer.frame = self.bounds
gradientLayer.colors = lineColors
gradientLayer.mask = lineLayer
layer.addSublayer(gradientLayer)
}
}
Well turns out I was searching more than an hour for this line:
gradientLayer.colors = lineColors
I forgot to map the UIColors objects in this array to CGColorRef objects...
This line fixed it for me:
gradientLayer.colors = lineColors.map({$0.cgColor})

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