How to create rounded corners with CAShapeLayer - ios

Is there a way to add rounded corners to a CAShapeLayer? In my case I needed the shape layer to create a dashed border via lineDashPattern.
^ notice how the dashed line is not rounded

The answer is simple. Create a bézier path with rounded corners.
UPDATE for Swift
view.clipsToBounds = true
view.layer.cornerRadius = 10.0
let border = CAShapeLayer()
border.path = UIBezierPath(roundedRect:view.bounds, cornerRadius:10.0).cgPath
border.frame = view.bounds
border.fillColor = nil
border.strokeColor = UIColor.purple.cgColor
border.lineWidth = borderWidth * 2.0 // doubled since half will be clipped
border.lineDashPattern = [15.0]
view.layer.addSublayer(border)
Objective-C
// (This old code assumes this is within a view with a custom property "border".)
self.clipsToBounds = YES;
self.layer.cornerRadius = 10.0;
self.border = [CAShapeLayer layer];
self.border.fillColor = nil;
self.border.path = [UIBezierPath bezierPathWithRoundedRect:self.bounds cornerRadius:10.0].cgPath;
self.border.frame = self.bounds;
self.border.strokeColor = [UIColor purpleColor].CGColor;
self.border.lineWidth = borderWidth * 2; // double desired width as half will be clipped
self.border.lineDashPattern = #[#15];
[self.layer addSublayer:self.border];

In swift 4 I created a UIView category (UIView+Borders) with the following function:
func borderDash(withRadius cornerRadius: Float, borderWidth: Float, borderColor: UIColor, dashSize: Int) {
let currentFrame = self.bounds
let shapeLayer = CAShapeLayer()
let path = CGMutablePath()
let radius = CGFloat(cornerRadius)
// Points - Eight points that define the round border. Each border is defined by two points.
let topLeftPoint = CGPoint(x: radius, y: 0)
let topRightPoint = CGPoint(x: currentFrame.size.width - radius, y: 0)
let middleRightTopPoint = CGPoint(x: currentFrame.size.width, y: radius)
let middleRightBottomPoint = CGPoint(x: currentFrame.size.width, y: currentFrame.size.height - radius)
let bottomRightPoint = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height)
let bottomLeftPoint = CGPoint(x: radius, y: currentFrame.size.height)
let middleLeftBottomPoint = CGPoint(x: 0, y: currentFrame.size.height - radius)
let middleLeftTopPoint = CGPoint(x: 0, y: radius)
// Points - Four points that are the center of the corners borders.
let cornerTopRightCenter = CGPoint(x: currentFrame.size.width - radius, y: radius)
let cornerBottomRightCenter = CGPoint(x: currentFrame.size.width - radius, y: currentFrame.size.height - radius)
let cornerBottomLeftCenter = CGPoint(x: radius, y: currentFrame.size.height - radius)
let cornerTopLeftCenter = CGPoint(x: radius, y: radius)
// Angles - The corner radius angles.
let topRightStartAngle = CGFloat(Double.pi * 3 / 2)
let topRightEndAngle = CGFloat(0)
let bottomRightStartAngle = CGFloat(0)
let bottmRightEndAngle = CGFloat(Double.pi / 2)
let bottomLeftStartAngle = CGFloat(Double.pi / 2)
let bottomLeftEndAngle = CGFloat(Double.pi)
let topLeftStartAngle = CGFloat(Double.pi)
let topLeftEndAngle = CGFloat(Double.pi * 3 / 2)
// Drawing a border around a view.
path.move(to: topLeftPoint)
path.addLine(to: topRightPoint)
path.addArc(center: cornerTopRightCenter,
radius: radius,
startAngle: topRightStartAngle,
endAngle: topRightEndAngle,
clockwise: false)
path.addLine(to: middleRightBottomPoint)
path.addArc(center: cornerBottomRightCenter,
radius: radius,
startAngle: bottomRightStartAngle,
endAngle: bottmRightEndAngle,
clockwise: false)
path.addLine(to: bottomLeftPoint)
path.addArc(center: cornerBottomLeftCenter,
radius: radius,
startAngle: bottomLeftStartAngle,
endAngle: bottomLeftEndAngle,
clockwise: false)
path.addLine(to: middleLeftTopPoint)
path.addArc(center: cornerTopLeftCenter,
radius: radius,
startAngle: topLeftStartAngle,
endAngle: topLeftEndAngle,
clockwise: false)
// Path is set as the shapeLayer object's path.
shapeLayer.path = path;
shapeLayer.backgroundColor = UIColor.clear.cgColor
shapeLayer.frame = currentFrame
shapeLayer.masksToBounds = false
shapeLayer.setValue(0, forKey: "isCircle")
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.strokeColor = borderColor.cgColor
shapeLayer.lineWidth = CGFloat(borderWidth)
shapeLayer.lineDashPattern = [NSNumber(value: dashSize), NSNumber(value: dashSize)]
shapeLayer.lineCap = kCALineCapRound
self.layer.addSublayer(shapeLayer)
self.layer.cornerRadius = radius;
}

Related

Add Shadow to CAShapeLayer with UIBezierPath

need some help adding a drop shadow to a line created with a CAShapeLayer / UIBezierPath
This code snippet produces the following line shape as in the screenshot below
let bounds = UIScreen.main.bounds
let shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 10.0
shapeLayer.frame = CGRect(x: 0, y: 0 , width: bounds.width, height: bounds.width)
shapeLayer.lineWidth = 5.0
shapeLayer.strokeColor = UIColor.red.cgColor
let offset : CGFloat = 20
let arcCenter = shapeLayer.position
let radius = shapeLayer.bounds.size.width / 2.0 - offset
let startAngle = CGFloat(0.0)
let endAngle = CGFloat(1.0 * .pi)
let circlePath = UIBezierPath(arcCenter: arcCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
shapeLayer.shadowOffset = CGSize(width: 0, height: 0)
shapeLayer.shadowColor = UIColor.red.cgColor
shapeLayer.shadowOpacity = 1.0
shapeLayer.shadowRadius = 25
shapeLayer.path = circlePath.cgPath
As you can see the shadow surrounds the outer part of the semi-circle shape.
Could anyone give me any hints on adding a drop shadow around the line only ?
Output:
UIView Extension:
extension UIView {
func renderCircle() {
let semiCircleLayer = CAShapeLayer()
let center = CGPoint (x: self.frame.size.width / 2, y: self.frame.size.height / 2)
let circleRadius = self.frame.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat(Double.pi * 2), endAngle: CGFloat(Double.pi), clockwise: true)
semiCircleLayer.path = circlePath.cgPath
semiCircleLayer.strokeColor = UIColor.red.cgColor
semiCircleLayer.fillColor = UIColor.clear.cgColor
semiCircleLayer.lineWidth = 8
semiCircleLayer.shadowColor = UIColor.red.cgColor
semiCircleLayer.shadowRadius = 25.0
semiCircleLayer.shadowOpacity = 1.0
semiCircleLayer.shadowPath = circlePath.cgPath.copy(strokingWithWidth: 25, lineCap: .round, lineJoin: .miter, miterLimit: 0)
semiCircleLayer.shadowOffset = CGSize(width: 1.0, height: 1.0)
self.layer.addSublayer(semiCircleLayer)
}
}
Usage:
semiCircleView.renderCircle()

Gradient Semi Circle

I want to draw semi circle with gradient, but when i add gradient the semi circle does not look right
what i have done
let center = CGPoint(x: circleView.bounds.size.width / 2, y: circleView.bounds.size.height / 2)
let circleRadius = circleView.bounds.size.width / 2
let circlePath = UIBezierPath(arcCenter: center, radius: circleRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * 2, clockwise: true)
let semiCircleLayer = CAShapeLayer()
semiCircleLayer.path = circlePath.cgPath
semiCircleLayer.lineCap = .round
semiCircleLayer.strokeColor = UIColor.white.cgColor
semiCircleLayer.fillColor = UIColor.clear.cgColor
semiCircleLayer.lineWidth = 6
semiCircleLayer.strokeStart = 0
semiCircleLayer.strokeEnd = 1
let gradient = CAGradientLayer()
gradient.colors = [UIColor.red.cgColor, UIColor.yellow.cgColor, UIColor.green.cgColor]
gradient.startPoint = CGPoint(x: 0.0, y: 0.5)
gradient.endPoint = CGPoint(x: 1.0, y: 0.5)
gradient.frame = circleView.bounds
gradient.mask = semiCircleLayer
circleView.layer.addSublayer(gradient)
Gradient Semi Circle:
Without Gradient Semi Circle:
Expected Result:
its because you're using full frame of initial circleView and lineWidth > 1
try to change this line
let circleRadius = circleView.bounds.size.width / 2 - 4

Swift 3.0 draw circle with same start and end angle results in line

I've written following code to draw a rect with a whole in it:
fileprivate let strokeWidth: CGFloat = 5
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.fillColor = UIColor(white: 0.5, alpha: 0.9).cgColor
let p = UIBezierPath(rect: bounds.insetBy(dx: -strokeWidth, dy: -strokeWidth))
let radius = bounds.size.width / 3
p.move(to: CGPoint(x: bounds.midX + radius, y: bounds.midY))
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: false)
p.close()
shapeLayer.path = p.cgPath
layer.addSublayer(shapeLayer)
The problem here is this line:
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 0, endAngle: CGFloat(2 * Double.pi), clockwise: false)
Printing out the description of the path:
UIBezierPath: 0x6000000b35c0; MoveTo {-5, -5}, LineTo {380, -5}, LineTo {380, 672}, LineTo {-5, 672}, Close, MoveTo {312.5, 333.5}, LineTo {312.5, 333.49999999999994}, Close
As you can see, the last two entries are lineTo and close, which gives me not the expected result (a full circle), I'll get nothing because the line is too short between 333.5 and 333.4999999.
This problem occurs since switching to Swift 3, in Objective-C this wasn't a problem.
Changing the end angle to 1.9 * Double.pi will also work, no idea why. But the full circle should have 2 * Double.pi.
Any idea or is it a Swift 3 bug?
Try like this:
let bounds = UIScreen.main.bounds
let strokeWidth: CGFloat = 5
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = strokeWidth
shapeLayer.fillColor = UIColor(white: 0.5, alpha: 0.9).cgColor
let p = UIBezierPath(rect: bounds.insetBy(dx: -strokeWidth, dy: -strokeWidth))
let radius = bounds.size.width / 3
p.move(to: CGPoint(x: bounds.midX + radius, y: bounds.midY))
p.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: radius, startAngle: 2 * .pi, endAngle: 0, clockwise: false)
p.close()
shapeLayer.fillColor = UIColor.red.cgColor
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 5
shapeLayer.path = p.cgPath
let view = UIView(frame: bounds)
view.backgroundColor = .yellow
view.layer.addSublayer(shapeLayer)
view

How to display image in diamond shape in swift?

I want to display images in diamond shape when I am giving width and height 120 and apply the corner radiu. I am getting diamond shape approximately but not getting exact diamond shape so any one suggest me it helpful to me.
self.imageView.layer.cornerRadius = self.imageView.frame.size.width / 2
self.imageView.clipsToBounds = true
If you have an image view and want to crop it to a diamond (rhombus) shape, you should:
Create UIBezierPath in diamond shape;
Use that as the path of a CAShapeLayer;
Set that CAShapeLayer as the mask of the UIImageView's layer
In Swift 3 and later, that might look like:
extension UIView {
func addDiamondMask(cornerRadius: CGFloat = 0) {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.midX, y: bounds.minY + cornerRadius))
path.addLine(to: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY - cornerRadius))
path.addLine(to: CGPoint(x: bounds.minX + cornerRadius, y: bounds.midY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = cornerRadius * 2
shapeLayer.lineJoin = kCALineJoinRound
shapeLayer.lineCap = kCALineCapRound
layer.mask = shapeLayer
}
}
So, just call addDiamondMask(cornerRadius:) (where the cornerRadius is optional), on your image view.
imageView.addDiamondMask()
That yields:
For Swift 2 rendition, see previous revision of this answer.
An alternate algorithm for rounding of corners might be:
extension UIView {
func addDiamondMask(cornerRadius: CGFloat = 0) {
let path = UIBezierPath()
let points = [
CGPoint(x: bounds.midX, y: bounds.minY),
CGPoint(x: bounds.maxX, y: bounds.midY),
CGPoint(x: bounds.midX, y: bounds.maxY),
CGPoint(x: bounds.minX, y: bounds.midY)
]
path.move(to: point(from: points[0], to: points[1], distance: cornerRadius, fromStart: true))
for i in 0 ..< 4 {
path.addLine(to: point(from: points[i], to: points[(i + 1) % 4], distance: cornerRadius, fromStart: false))
path.addQuadCurve(to: point(from: points[(i + 1) % 4], to: points[(i + 2) % 4], distance: cornerRadius, fromStart: true), controlPoint: points[(i + 1) % 4])
}
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.clear.cgColor
layer.mask = shapeLayer
}
private func point(from point1: CGPoint, to point2: CGPoint, distance: CGFloat, fromStart: Bool) -> CGPoint {
let start: CGPoint
let end: CGPoint
if fromStart {
start = point1
end = point2
} else {
start = point2
end = point1
}
let angle = atan2(end.y - start.y, end.x - start.x)
return CGPoint(x: start.x + distance * cos(angle), y: start.y + distance * sin(angle))
}
}
Here I'm doing quad bezier in the corners, but I think the effect for rounded corners is slightly better than the above if the diamond is at all elongated.
Anyway, that yields:
SWIFT 5
extension UIView {
func addDiamondMask(cornerRadius: CGFloat = 0) {
let path = UIBezierPath()
path.move(to: CGPoint(x: bounds.midX, y: bounds.minY + cornerRadius))
path.addLine(to: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.midY))
path.addLine(to: CGPoint(x: bounds.midX, y: bounds.maxY - cornerRadius))
path.addLine(to: CGPoint(x: bounds.minX + cornerRadius, y: bounds.midY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.strokeColor = UIColor.white.cgColor
shapeLayer.lineWidth = cornerRadius * 2
shapeLayer.lineJoin = .round
shapeLayer.lineCap = .round
layer.mask = shapeLayer
}
}

custom UIButton with UIBezierPath

I'm making my own custom UIButton.
I subcalssed the UIButton class and drawRect function
this is my code:
override func drawRect(rect: CGRect)
{
let midPoint = CGPointMake(rect.midX, rect.midY)
if !self.selected
{
let redCircle = UIBezierPath()
let redCircleRadius = rect.width - self.whiteCirlceWidth - self.spaceBetweenCirclesWidth
redCircle.addArcWithCenter(CGPointMake(rect.midX, rect.midY), radius: redCircleRadius,
startAngle: 0 , endAngle: 2 * CGFloat(M_PI), clockwise: true)
let shapeLayer = CAShapeLayer()
shapeLayer.path = redCircle.CGPath
shapeLayer.fillColor = UIColor.redColor().CGColor
shapeLayer.strokeColor = UIColor.blackColor().CGColor
shapeLayer.lineWidth = 4.0
self.layer.addSublayer(shapeLayer)
let whiteCircle = UIBezierPath()
whiteCircle.addArcWithCenter(midPoint,
radius: self.bounds.width / 2,
startAngle: 0, endAngle: 2 * CGFloat(M_PI) , clockwise: true)
UIColor.whiteColor().setFill()
whiteCircle.fill()
}
else
{
let whiteCircle = UIBezierPath()
whiteCircle.addArcWithCenter(midPoint,
radius: self.bounds.width / 2,
startAngle: 0, endAngle: 2 * CGFloat(M_PI) , clockwise: true)
UIColor.whiteColor().setFill()
whiteCircle.fill()
let blankCircle = UIBezierPath()
blankCircle.addArcWithCenter(midPoint, radius: rect.width - self.whiteCirlceWidth - self.spaceBetweenCirclesWidth,
startAngle: 0, endAngle: 2 * CGFloat(M_PI), clockwise: true)
UIColor.blackColor().setFill()
blankCircle.fill()
var rect = CGRect(origin: midPoint, size: CGSizeMake(rect.width / 2, rect.width / 2))
rect.origin.x -= rect.origin.x / 2
rect.origin.y -= rect.origin.y / 2
let roundedRect = UIBezierPath(roundedRect: rect, cornerRadius: 3)
UIColor.redColor().setFill()
roundedRect.fill()
}
}
when I click on the button I do this
#IBAction func recordBtnClicked(sender : UIButton)
{
self.recordBtn.selected = !self.recordBtn.selected
self.recordBtn.setNeedsDisplay()
}
but it doesn't change anything!.
I checked in the debugger and it does the right block of code .
I checked two blocks and they do the right drawing
what's wrong?
I figured it out it was because I added a subLayer and it stucks and I didn't remove it ' so I changed the code and didn't used the sublayer.
you can remove the subLayer each time you call drawRect

Resources