CAShapeLayer with UIBezierPath - ios

I wanted to display a triangle shaped view in a UITableView cell like this.
I managed to accomplish this using the blow code.
import UIKit
class TriangleView: UIView {
override func drawRect(rect: CGRect) {
let width = self.layer.frame.width
let height = self.layer.frame.height
let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 0, 0)
CGPathAddLineToPoint(path, nil, width, 0)
CGPathAddLineToPoint(path, nil, 0, height)
CGPathAddLineToPoint(path, nil, 0, 0)
CGPathCloseSubpath(path)
let mask = CAShapeLayer()
mask.frame = self.layer.bounds
mask.path = path
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path
shape.fillColor = UIColor.clearColor().CGColor
self.layer.insertSublayer(shape, atIndex: 0)
}
}
While I was searching how to create shapes in UIViews, I discovered that you could use UIBezierPath to do the same. So I tried replicating the same thing using UIBezierPath.
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.moveToPoint(CGPoint(x: width, y: 0))
path.moveToPoint(CGPoint(x: 0, y: height))
path.moveToPoint(CGPoint(x: 0, y: 0))
path.closePath()
let mask = CAShapeLayer()
mask.frame = self.bounds
mask.path = path.CGPath
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path.CGPath
shape.fillColor = UIColor.clearColor().CGColor
self.layer.insertSublayer(shape, atIndex: 0)
But this simply does't work. No shape is getting displayed.
Do I need to do anything extra in order to get this working?

You're making this harder than it needs to be. You only need to add a shape layer with a path, and set the fillColor of that layer. The main reason your code doesn't work though, is because you're using moveToPoint for all you lines instead of addLineToPoint for all but the first one.
class TriangleView: UIView {
required init(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(frame: CGRect) {
super.init(frame: frame)
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 0, y: 0))
path.addLineToPoint(CGPoint(x: frame.size.width, y: 0))
path.addLineToPoint(CGPoint(x: 0, y: frame.size.height))
path.closePath()
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path.CGPath
shape.fillColor = UIColor.blueColor().CGColor
self.layer.addSublayer(shape)
}
}

The problem should be the mask on layer and the color of shape.
The following code work for me.
class TriangleView: UIView {
override func drawRect(rect: CGRect) {
let width:CGFloat = bounds.width/9
let height:CGFloat = bounds.width/9
let path = UIBezierPath()
path.moveToPoint(bounds.origin)
path.addLineToPoint(CGPoint(x: width, y: bounds.origin.y))
path.addLineToPoint(CGPoint(x: bounds.origin.x, y: height))
// closePath will connect back to start point
path.closePath()
let shape = CAShapeLayer()
shape.path = path.CGPath
shape.fillColor = UIColor.blueColor().CGColor
self.layer.insertSublayer(shape, atIndex: 0)
// Setting text margin
textContainerInset = UIEdgeInsetsMake(8, width, 8, 0)
}
}

Related

How to draw a custom rounded rectangle in SWIFT?

I'm trying to draw a shape shown on the upper image programmatically.
This shape has custom rounded corners.
view.layer.cornerRadius = some value less than half diameter
This didn't work. Setting cornerRadius draws straight lines on every side(as seen on the bottom image) but the shape I'm trying to draw has no straight lines at all and it's not an oval.
I also tried below without luck. This code just draws an oval.
var path = UIBezierPath(ovalIn: CGRect(x: 000, y: 000, width: 000, height: 000))
I believe this can not be done by setting cornerRadius.
There should be something more.
I have no idea what class should I use and how.
Please anybody give me some direction.
Thanks!
I accomplished this by drawing a quadratic.
let dimention: CGFloat = some value
let path = UIBezierPath()
path.move(to: CGPoint(x: dimension/2, y: 0))
path.addQuadCurve(to: CGPoint(x: dimension, y: dimension/2),
controlPoint: CGPoint(x: dimension, y: 0))
path.addQuadCurve(to: CGPoint(x: dimension/2, y: dimension),
controlPoint: CGPoint(x: dimension, y: dimension))
path.addQuadCurve(to: CGPoint(x: 0, y: dimension/2),
controlPoint: CGPoint(x: 0, y: dimension))
path.addQuadCurve(to: CGPoint(x: dimension/2, y: 0),
controlPoint: CGPoint(x: 0, y: 0))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.strokeColor = UIColor.blue.cgColor
shapeLayer.fillColor = UIColor.clear.cgColor
shapeLayer.lineWidth = 1.0
shapeLayer.position = CGPoint(x: 0, y: 0)
self.someView.layer.addSublayer(shapeLayer)
You have to add cipsToBounds to the code for getting the image with given cornerRadius.
Try the blow codes ,
view.layer.cornerRadius = some value
view.clipsToBounds = true
You can use following path for your custom shape
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: 150, height: 150), byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 8, height: 8))
You can change width and height according to your requirements and UI
class RoundView: UIView {
var roundCorner: UIRectCorner? = nil
var roundRadius:CGFloat = 0
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
private func commonInit() {
Bundle.main.loadNibNamed(“nib name”, owner: self, options: nil))
addSubview(contentView)
contentView.frame = self.bounds
contentView.autoresizingMask = [.flexibleHeight, .flexibleWidth]
isUserInteractionEnabled = true
}
override func layoutSubviews() {
super.layoutSubviews()
if roundCorner != nil {
// self.roundCorners([.topRight, .bottomLeft, .bottomRight], radius: 15)
self.roundCorners(roundCorner!, radius: roundRadius)
}
}
}

Adding a mask to CAGradientLayer makes UIBezierPath disappear

I want to add an inner border to a view with a gradient. The following code works and gives me this result
import UIKit
class InnerGradientBorderView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clear
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clear
}
override func draw(_ rect: CGRect) {
super.draw(rect)
addGradientInnerBorder(width: 8, color: FlatWhite())
}
func addGradientInnerBorder(width: CGFloat, color: UIColor) {
// Setup
let topLeftO = CGPoint(x: 0, y: 0)
let topLeftI = CGPoint(x: width, y: width)
let topRightO = CGPoint(x: frame.width, y: 0)
let topRightI = CGPoint(x: frame.width - width, y: width)
let bottomLeftO = CGPoint(x: 0, y: frame.height)
let bottomLeftI = CGPoint(x: width, y: frame.height - width)
let bottomRightO = CGPoint(x: frame.width, y: frame.height)
let bottomRightI = CGPoint(x: frame.width - width, y: frame.height - width)
// Top
let topPoints = [topLeftO, topLeftI, topRightI, topRightO, topLeftO]
let topGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 0, y: 1)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: topPoints), color: color, gradientPoints: topGradientPoints)
// Left
let leftPoints = [topLeftO, topLeftI, bottomLeftI, bottomLeftO, topLeftO]
let leftGradientPoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: leftPoints), color: color, gradientPoints: leftGradientPoints)
// Right
let rightPoints = [topRightO, topRightI, bottomRightI, bottomRightO, topRightO]
let rightGradientPoints = [CGPoint(x: 1, y: 0), CGPoint(x: 0, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: rightPoints), color: color, gradientPoints: rightGradientPoints)
// Bottom
let bottomPoints = [bottomLeftO, bottomLeftI, bottomRightI, bottomRightO, bottomLeftO]
let bottomGradientPoints = [CGPoint(x: 0, y: 1), CGPoint(x: 0, y: 0)]
addGradientToBeizerPath(path: addClosedPathForPoints(points: bottomPoints), color: color, gradientPoints: bottomGradientPoints)
}
func addClosedPathForPoints(points: [CGPoint]) -> UIBezierPath? {
guard points.count == 5 else { return nil }
let path = UIBezierPath()
path.move(to: points[0])
path.addLine(to: points[1])
path.addLine(to: points[2])
path.addLine(to: points[3])
path.addLine(to: points[4])
path.close()
return path
}
func addGradientToBeizerPath(path: UIBezierPath?, color: UIColor, gradientPoints: [CGPoint]) {
guard let path = path, gradientPoints.count == 2 else { return }
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.colors = [color.cgColor, UIColor.clear.cgColor]
gradient.startPoint = gradientPoints[0]
gradient.endPoint = gradientPoints[1]
// let shapeMask = CAShapeLayer()
// shapeMask.path = path.cgPath
// gradient.mask = shapeMask
self.layer.insertSublayer(gradient, at: 0)
}
}
You will notice that the edges do not look that great.To fix that, I am giving the edges an angle. When I apply a mask to this gradient with this angle, the right and bottom paths disappear like this:
All I am doing here is using some closed bezierPaths and applying a gradient to them. If the gradient has a mask (the commented code is uncommented), two of the paths disappear. I have a feeling that I am not understanding something so hopefully someone here can tell me how to use CAShapeLayer properly.
This comment to CALayer mask property
explains it perfectly:
The mask layer lives in the masked layer's coordinate system just as if it were a sublayer.
In your case, the origin of the right and bottom gradient layer is not
at (0, 0) of the enclosing view, but at (frame.width - width, 0)
and (frame.height - width, 0) respectively.
On the other hand, the coordinates of the points in
oshapeMask.path are relative to (0, 0) of the enclosing view.
A possible simple fix is to transform the coordinate system of
the shape layer so that it uses the same coordinates as the points
of the given path:
let gradient = CAGradientLayer()
gradient.frame = path.bounds
gradient.bounds = path.bounds // <<--- ADDED HERE!
gradient.colors = [color.cgColor, UIColor.clear.cgColor]
gradient.startPoint = gradientPoints[0]
gradient.endPoint = gradientPoints[1]
let shapeMask = CAShapeLayer()
shapeMask.path = path.cgPath
gradient.mask = shapeMask
self.layer.addSublayer(gradient)

Fill Path on Intersecting UIBezierPath

Any idea on how to fill all the paths in here. What is currently happening is that I draw a rectangle path on my view then add small circles in between but it seems that if the circle and rectangle intersects, the white fill color is showing. What I would want is show still the gradient layer. Any help? My current code is below.
func addGradientLayer() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = bounds
let startColor = UIColor.create(withHexOrName: OurPayStatesViewUX.GradientColorStart)
let endColor = UIColor.create(withHexOrName: OurPayStatesViewUX.GradientColorEnd)
gradientLayer.colors = [startColor.CGColor, endColor.CGColor]
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 1, y: 0)
layer.insertSublayer(gradientLayer, atIndex: 0)
}
func createOverlay(view: UIView, circleLocations: [CGPoint]) {
maskLayer?.removeFromSuperlayer()
let radius: CGFloat = view.frame.height/2
let path = UIBezierPath(roundedRect: CGRect(x: 0, y: 0, width: view.bounds.size.width, height: view.bounds.size.height), cornerRadius: 0)
for i in circleLocations {
// Create a circle path in each of the state views
let circlePath = UIBezierPath(roundedRect: CGRect(x: i.x, y: i.y, width: 2 * radius, height: 2 * radius), cornerRadius: radius)
path.appendPath(circlePath)
}
let rect = createRectangle(startPointX: 0, endPointX: view.bounds.size.width)
path.appendPath(rect)
path.usesEvenOddFillRule = true
let fillLayer = CAShapeLayer()
fillLayer.path = path.CGPath
fillLayer.fillRule = kCAFillRuleEvenOdd
fillLayer.fillColor = backgroundColor?.CGColor ?? UIColor.whiteColor().CGColor
fillLayer.opacity = 1
maskLayer = fillLayer
layer.addSublayer(fillLayer)
}
func createRectangle(startPointX startPointX: CGFloat, endPointX: CGFloat) -> UIBezierPath {
let rectHeight: CGFloat = 6
let path = UIBezierPath(rect: CGRect(x: startPointX, y: frame.height/2 - rectHeight/2, width: endPointX - startPointX, height: rectHeight))
return path
}

Draw line with oblique border

How to draw line with oblique border like this:
I drew only a normal line:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 30.f);
CGContextSetStrokeColorWithColor(context, [UIColor personalBlue].CGColor);
CGContextMoveToPoint(context, 0.f, 0.f);
CGContextAddLineToPoint(context,self.bounds.size.width, 0.f);
CGContextStrokePath(context);
I use CAShapeLayer:
class TriangleView: UIView {
override func awakeFromNib() {
super.awakeFromNib()
self.contentMode = UIViewContentMode.redraw
}
override func draw(_ rect: CGRect) {
let mask = CAShapeLayer()
mask.frame = self.layer.bounds
let width = self.layer.frame.size.width
let height = self.layer.frame.size.height
let path = CGMutablePath.init()
path.move(to: CGPoint(x: width, y: 0))
path.addLine(to: CGPoint(x: width, y: height))
path.addLine(to: CGPoint(x: 0, y: height))
path.addLine(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 50))
mask.path = path
self.layer.mask = mask
let shape = CAShapeLayer()
shape.frame = self.bounds
shape.path = path
shape.lineWidth = 3.0
shape.strokeColor = UIColor.white.cgColor
shape.fillColor = UIColor.white.cgColor
self.layer.insertSublayer(shape, at: 0)
}
}
The result is this:

How do I create a UIView with a transparent circle inside (in swift)?

I want to create a view that looks like this:
I figure what I need is a uiview with some sort of mask, I can make a mask in the shape of a circle using a UIBezierpath, however I cannot invert this makes so that it masks everything but the circle. I need this to be a mask of a view and not a fill layer because the view that I intend to mask has a UIBlurEffect on it. The end goal is to animate this UIView overtop of my existing views to provide instruction.
Please note that I am using swift. Is there away to do this? If so, how?
Updated again for Swift 4 & removed a few items to make the code tighter.
Please note that maskLayer.fillRule is set differently between Swift 4 and Swift 4.2.
func createOverlay(frame: CGRect,
xOffset: CGFloat,
yOffset: CGFloat,
radius: CGFloat) -> UIView {
// Step 1
let overlayView = UIView(frame: frame)
overlayView.backgroundColor = UIColor.black.withAlphaComponent(0.6)
// Step 2
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset),
radius: radius,
startAngle: 0.0,
endAngle: 2.0 * .pi,
clockwise: false)
path.addRect(CGRect(origin: .zero, size: overlayView.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = kCAFillRuleEvenOdd
// For Swift 4.2
maskLayer.fillRule = .evenOdd
// Step 4
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
A rough breakdown on what is happening:
Create a view sized to the specified frame, with a black background set to 60% opacity
Create the path for drawing the circle using the provided starting point and radius
Create the mask for the area to remove
Apply the mask & clip to bounds
The following code snippet will call this and place a circle in the middle of the screen with radius of 50:
let overlay = createOverlay(frame: view.frame,
xOffset: view.frame.midX,
yOffset: view.frame.midY,
radius: 50.0)
view.addSubview(overlay)
Which looks like this:
You can use this function to create what you need.
func createOverlay(frame : CGRect)
{
let overlayView = UIView(frame: frame)
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.blackColor()
self.view.addSubview(overlayView)
let maskLayer = CAShapeLayer()
// Create a path with the rectangle in it.
var path = CGPathCreateMutable()
let radius : CGFloat = 50.0
let xOffset : CGFloat = 10
let yOffset : CGFloat = 10
CGPathAddArc(path, nil, overlayView.frame.width - radius/2 - xOffset, yOffset, radius, 0.0, 2 * 3.14, false)
CGPathAddRect(path, nil, CGRectMake(0, 0, overlayView.frame.width, overlayView.frame.height))
maskLayer.backgroundColor = UIColor.blackColor().CGColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
// Release the path since it's not covered by ARC.
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
}
Adjust the radius and xOffset and yOffset to change the radius and position of the circle.
For Swift 3, here is rakeshbs' answer formatted so it returns the UIView needed:
func createOverlay(frame : CGRect, xOffset: CGFloat, yOffset: CGFloat, radius: CGFloat) -> UIView
{
let overlayView = UIView(frame: frame)
overlayView.alpha = 0.6
overlayView.backgroundColor = UIColor.black
// Create a path with the rectangle in it.
let path = CGMutablePath()
path.addArc(center: CGPoint(x: xOffset, y: yOffset), radius: radius, startAngle: 0.0, endAngle: 2 * 3.14, clockwise: false)
path.addRect(CGRect(x: 0, y: 0, width: overlayView.frame.width, height: overlayView.frame.height))
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.black.cgColor
maskLayer.path = path;
maskLayer.fillRule = kCAFillRuleEvenOdd
// Release the path since it's not covered by ARC.
overlayView.layer.mask = maskLayer
overlayView.clipsToBounds = true
return overlayView
}
The above solution works great.
Say if you are looking for mask with rectangle area here is the snippet below
let fWidth = self.frame.size.width
let fHeight = self.frame.size.height
let squareWidth = fWidth/2
let topLeft = CGPoint(x: fWidth/2-squareWidth/2, y: fHeight/2-squareWidth/2)
let topRight = CGPoint(x: fWidth/2+squareWidth/2, y: fHeight/2-squareWidth/2)
let bottomLeft = CGPoint(x: fWidth/2-squareWidth/2, y: fHeight/2+squareWidth/2)
let bottomRight = CGPoint(x: fWidth/2+squareWidth/2, y: fHeight/2+squareWidth/2)
let cornerWidth = squareWidth/4
// Step 2
let path = CGMutablePath()
path.addRoundedRect(in: CGRect(x: topLeft.x, y: topLeft.y,
width: topRight.x - topLeft.x, height: bottomLeft.y - topLeft.y),
cornerWidth: 20, cornerHeight: 20)
path.addRect(CGRect(origin: .zero, size: self.frame.size))
// Step 3
let maskLayer = CAShapeLayer()
maskLayer.backgroundColor = UIColor.clear.cgColor
maskLayer.path = path
// For Swift 4.0
maskLayer.fillRule = kCAFillRuleEvenOdd
// For Swift 4.2
//maskLayer.fillRule = .evenOdd
// Step 4
self.layer.mask = maskLayer
self.clipsToBounds = true
rectangle mask looks like this

Resources