How to draw a custom rounded rectangle in SWIFT? - ios

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)
}
}
}

Related

How to draw a curve like this in UIBezierPath Swift?

I am trying to develop a screen whose background looks like this:
Here I am trying to develop the gray curved background and it fills the lower part of the screen as well. I'm very new to UIBezierPath and I've tried this:
class CurvedView: UIView {
//MARK:- Data Types
//MARK:- View Setup
override func draw(_ rect: CGRect) {
let fillColor: UIColor = .blue
let path = UIBezierPath()
let y:CGFloat = 0
print(rect.height)
print(rect.width)
path.move(to: CGPoint(x: .zero, y: 100))
path.addLine(to: CGPoint(x: 60, y: 100))
path.addCurve(to: .init(x: 100, y: 0), controlPoint1: .init(x: 125, y: 80), controlPoint2: .init(x: 50, y: 80))
path.close()
fillColor.setFill()
path.fill()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .init(hex: "#dfe1e3")
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = .init(hex: "#dfe1e3")
}
}
This code gave me this:
I followed a lot of tutorials but I didn't get the exact understanding. I understood that for this curve I have to move to (0,100) and then add a line and then add a curve and ten extend the line add a curve then straight line lower curve and then straight line and close. But, when I started as you can see the blue line didn't cover the upper part. Can any one please help me?
Here some example that I create, you can change the value to make it more similar to what you want
Here a guide how control point in a curve work
Note: I called this code in viewDidload
let path = UIBezierPath()
let fillColor = UIColor.blue
let y: CGFloat = UIScreen.main.bounds.size.height
let x: CGFloat = UIScreen.main.bounds.size.width
let height: CGFloat = 200
path.move(to: CGPoint(x: 0, y: y)) // bottom left
path.addLine(to: CGPoint(x: 0, y: y - 20)) // top left
path.addCurve(to: CGPoint(x: x, y: y - height), controlPoint1: CGPoint(x: x * 2 / 3, y: y), controlPoint2: CGPoint(x: x * 5 / 6, y: y - height * 6 / 5)) // curve to top right
path.addLine(to: CGPoint(x: x, y: y)) // bottom right
path.close() // close the path from bottom right to bottom left
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = fillColor.cgColor
view.layer.addSublayer(shapeLayer)
in reference to #aiwiguna
class CurvedView: UIView {
//MARK:- Data Types
//MARK:- View Setup
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
let fillColor = UIColor.blue
let y: CGFloat = UIScreen.main.bounds.size.height
let x: CGFloat = UIScreen.main.bounds.size.width
let height: CGFloat = 200
path.move(to: CGPoint(x: 0, y: y)) // bottom left
path.addLine(to: CGPoint(x: 0, y: y - 20)) // top left
path.addCurve(to: CGPoint(x: x, y: y - height), controlPoint1: CGPoint(x: x * 2 / 3, y: y), controlPoint2: CGPoint(x: x * 5 / 6, y: y - height * 6 / 5)) // curve to top right
path.addLine(to: CGPoint(x: x, y: y)) // bottom right
path.close() // close the path from bottom right to bottom left
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = fillColor.cgColor
path.close()
fillColor.setFill()
path.fill()
}
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .red
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.backgroundColor = .yellow
}
}

Trim UIView with 2 arcs

I have a UIView and I want to trim it with two circles, like I've drawn(sorry for the quality).
My code:
final class TrimmedView: UIView {
override init(frame: CGRect) {
super.init(frame: frame)
let size = CGSize(width: 70, height: 70)
let innerRadius: CGFloat = 366.53658283002471
let innerBottomRadius: CGFloat = 297.88543112651564
let path = UIBezierPath()
path.move(to: CGPoint(x: -innerRadius + (size.width / 2), y: innerRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerRadius), radius: innerRadius, startAngle: CGFloat.pi, endAngle: 0, clockwise: true)
path.move(to: CGPoint(x: -innerBottomRadius + (size.width / 2), y: innerBottomRadius))
path.addArc(withCenter: CGPoint(x: size.width / 2, y: innerBottomRadius), radius: innerBottomRadius, startAngle: 0, endAngle: CGFloat.pi, clockwise: true)
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.shadowPath = path.cgPath
layer.mask = shapeLayer
}
required init?(coder: NSCoder) {
super.init(coder: coder)
}
}
ViewController:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let view = UIView(frame: CGRect(origin: CGPoint(x: (self.view.bounds.width - 70) / 2, y: (self.view.bounds.height - 70) / 2), size: CGSize(width: 70, height: 70)))
view.backgroundColor = .red
self.view.addSubview(view)
let view1 = TrimmedView(frame: view.frame)
view1.backgroundColor = .yellow
self.view.addSubview(view1)
}
I got this result. It seems for me that top trimming works but the bottom doesn't and I don't know why. Any help would be appreciated. Thanks.
Here is a custom view that should give you what you want.
The UIBezierPath uses QuadCurves for the top "convex" arc and the bottom "concave" arc.
It is marked #IBDesignable so you can see it at design-time in IB / Storyboard. The "height" of the arc and the fill color are each set as #IBInspectable so you can adjust those values at design-time as well.
To use it in Storyboard:
Add a normal UIView
change the Class to BohdanShapeView
in the Attributes Inspector pane, set the Arc Offset and the Fill Color
set the background color as with a normal view (you'll probably use clear)
Result:
To use it via code:
let view1 = BohdanShapeView(frame: view.frame)
view1.fillColor = .systemTeal
view1.arcOffset = 10
self.view.addSubview(view1)
Here is the class:
#IBDesignable
class BohdanShapeView: UIView {
#IBInspectable var arcOffset: CGFloat = 0.0
#IBInspectable var fillColor: UIColor = UIColor.white
let shapeLayer = CAShapeLayer()
override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
commonInit()
}
func commonInit() -> Void {
// add the shape layer
layer.addSublayer(shapeLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
// fill color for the shape
shapeLayer.fillColor = self.fillColor.cgColor
let width = bounds.size.width
let height = bounds.size.height
let bezierPath = UIBezierPath()
// start at arcOffset below top-left
bezierPath.move(to: CGPoint(x: 0.0, y: 0.0 + arcOffset))
// add curve to arcOffset below top-right
bezierPath.addQuadCurve(to: CGPoint(x: width, y: 0.0 + arcOffset), controlPoint: CGPoint(x: width * 0.5, y: 0.0 - arcOffset))
// add line to bottom-right
bezierPath.addLine(to: CGPoint(x: width, y: height))
// add curve to bottom-left
bezierPath.addQuadCurve(to: CGPoint(x: 0.0, y: height), controlPoint: CGPoint(x: width * 0.5, y: height - arcOffset * 2.0))
// close the path
bezierPath.close()
shapeLayer.path = bezierPath.cgPath
}
}

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)

CAShapeLayer groove path

I wanted to make a white color view with a triangle shaped pointer being grooved inside like this:
As shown in the image above, goal is to create a "rounded groove" inset into the whiteview
let pointerRadius:CGFloat = 4
pointerLayer = CAShapeLayer()
pointerLayer.path = pointerPathForContentSize(contentSize: bounds.size).cgPath
pointerLayer.lineJoin = kCALineJoinRound
pointerLayer.lineWidth = 2*pointerRadius
pointerLayer.fillColor = UIColor.white.cgColor
pointerLayer.strokeColor = UIColor.black.cgColor
pointerLayer.backgroundColor = UIColor.white.cgColor
layer.addSublayer(pointerLayer)
But what I get is this:
But,if I set the stroke color to white
pointerLayer.strokeColor = UIColor.white.cgColor
In the groove I wanted to have a rounded edge in bottom (just like in the first pic) which no more remains visible when fillColor and strokeColor get matched (both white).
How can I fix it?
Is there any other way to achieve this?
Here is the code for pointer path:
private func pointerPathForContentSize(contentSize: CGSize) -> UIBezierPath
{
let rect = CGRect(x: 0, y: 0, width: contentSize.width, height: contentSize.height)
let width:CGFloat = 20
let height:CGFloat = 20
let path = UIBezierPath()
let startX:CGFloat = 50
let startY:CGFloat = rect.minY
path.move(to: CGPoint(x: startX , y: startY))
path.addLine(to: CGPoint(x: (startX + width*0.5), y: startY + height))
path.addLine(to: CGPoint(x: (startX + width), y: startY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY))
path.close()
return path
}
Since you have already outlined the shape you want by stroking the path, I think the simplest solution is probably to use the stroked and filled path as a mask.
For example, here is a rectangular red view:
And here is the same red view with the notch cut out of the top. This seems to be the sort of thing you're after:
What I did there was to mask the red view with a special mask view that draws the notch using .clear blend mode:
class MaskView : UIView {
override init(frame: CGRect) {
super.init(frame:frame)
self.isOpaque = false
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
let con = UIGraphicsGetCurrentContext()!
con.fill(self.bounds)
con.setBlendMode(.clear)
con.move(to: CGPoint(x:0, y:-4))
con.addLine(to: CGPoint(x:100, y:-4))
con.addLine(to: CGPoint(x:110, y:15))
con.addLine(to: CGPoint(x:120, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-4))
con.addLine(to: CGPoint(x: self.bounds.maxX, y:-20))
con.addLine(to: CGPoint(x: 0, y:-20))
con.closePath()
con.setLineJoin(.round)
con.setLineWidth(10)
con.drawPath(using: .fillStroke) // stroke it and fill it
}
}
So then when I'm ready to cut out the notch on the red view, I just say:
self.redView.mask = MaskView(frame:self.redView.bounds)

Create CAGradient Layer with transparent hole in it?

Currently I do it like this:
final class BorderedButton: BottomNavigationButton {
private let gradient = CAGradientLayer()
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
let radius = bounds.size.height / 2
let outside = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height), cornerRadius: radius)
let inside = UIBezierPath(roundedRect: CGRect(x: 3.0, y: 3.0, width: bounds.width - 6, height: bounds.height - 6), cornerRadius: radius - 3)
outside.append(inside)
outside.usesEvenOddFillRule = true
shapeLayer.path = outside.cgPath
}
init(color: UIColor?) {
super.init(frame: .zero)
let isGradient = color == nil
shapeLayer.fillRule = kCAFillRuleEvenOdd
shapeLayer.fillColor = UIColor.red.cgColor
layer.insertSublayer(shapeLayer, at: 0)
//gradient part
gradient.colors = isGradient ? [Constants.gradientStart, Constants.gradientEnd] : [color!.cgColor, color!.cgColor]
gradient.startPoint = CGPoint(x: 0.2, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
}
}
How can I apply that gradient to my code?
Don't add the CAShapeLayer to the view's layer, rather set it as the mask of the CAGradientLayer. Also don't forget to set the bounds of the gradient layer.
I had to make some modifications to get it to run in a playground, but this works for me:
let gradientStart = UIColor.orange
let gradientEnd = UIColor.blue
final class BorderedButton: UIButton {
private let gradient = CAGradientLayer()
private let shapeLayer = CAShapeLayer()
override func layoutSubviews() {
super.layoutSubviews()
let radius = bounds.size.height / 2
let outside = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: bounds.width, height: bounds.height), cornerRadius: radius)
let inside = UIBezierPath(roundedRect: CGRect(x: 3.0, y: 3.0, width: bounds.width - 6, height: bounds.height - 6), cornerRadius: radius - 3)
outside.append(inside)
outside.usesEvenOddFillRule = true
shapeLayer.path = outside.cgPath
gradient.frame = CGRect(x: 0, y: 0, width: bounds.size.width, height: bounds.size.height)
}
init(color: UIColor?, frame: CGRect = .zero) {
super.init(frame: frame)
let isGradient = color == nil
shapeLayer.fillRule = kCAFillRuleEvenOdd
//gradient part
gradient.colors = isGradient ? [gradientStart.cgColor, gradientEnd.cgColor] : [color!.cgColor, color!.cgColor]
gradient.startPoint = CGPoint(x: 0.2, y: 0.5)
gradient.endPoint = CGPoint(x: 1, y: 1)
gradient.mask = shapeLayer
layer.addSublayer(gradient)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}

Resources