How to add rounded corner to a UIBezierPath custom rectangle?
Instead of starting the code with a straight line :
path.moveToPoint(CGPoint(x: 300, y: 0))
I instead start with an arc (upper right):
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
and by doing this, I have four rounded corners and I just need to add a straight line at the end of the code right before:
path.closePath()
Here is the code and a screenshot:
let path = UIBezierPath()
path.addArcWithCenter(CGPoint(x: 300-10, y: 50), radius: 10 , startAngle: 0 , endAngle: CGFloat(M_PI/2) , clockwise: true) //1st rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 50), radius:10, startAngle: CGFloat(2 * M_PI / 3), endAngle:CGFloat(M_PI) , clockwise: true)// 2rd rounded corner
path.addArcWithCenter(CGPoint(x: 200, y: 10), radius:10, startAngle: CGFloat(M_PI), endAngle:CGFloat(3 * M_PI / 2), clockwise: true)// 3rd rounded corner
// little triangle
path.addLineToPoint(CGPoint(x:240 , y:0))
path.addLineToPoint(CGPoint(x: 245, y: -10))
path.addLineToPoint(CGPoint(x:250, y: 0))
path.addArcWithCenter(CGPoint(x: 290, y: 10), radius: 10, startAngle: CGFloat(3 * M_PI / 2), endAngle: CGFloat(2 * M_PI ), clockwise: true)
path.addLineToPoint(CGPoint(x:300 , y:50))
path.closePath()
A couple of observations:
Make sure that you take the view
bounds
and inset it by half of the line width. That ensures that the entire stroked border falls within thebounds
of the view. If your line width is 1, this might not be so obvious, but with larger line widths, the problem becomes more pronounced.If using
draw(_:)
method, don’t use therect
that is passed to this method, but rather refer to thebounds
(inset, as described above). TheCGRect
passed todraw(_:)
is the rectangle being drawn, not necessarily the fullbounds
. (It generally is, but not always, so always refer to thebounds
of the view, not therect
passed to this method.)As the documentation says (emphasis added):
The portion of the view’s bounds that needs to be updated. The first time your view is drawn, this rectangle is typically the entire visible bounds of your view. However, during subsequent drawing operations, the rectangle may specify only part of your view.
I’d give all of the the various properties of the view a
didSet
observer that will trigger the view to be redrawn. That way, any IB overrides or programmatically set values will be reflected in the resulting view automatically.If you want, you can make the whole thing
@IBDesignable
and make the properties@IBInspectable
, so you can see this rendered in Interface Builder. It’s not necessary, but can be useful if you want to see this rendered in storyboards or NIBs.While you can round corners using a circular arc, using a quad curve is easier, IMHO. You just specify where the arc ends and the corner of the rectangle, and the quadratic bezier will produce a nicely rounded corner. Using this technique, no calculation of angles or the center of the arc is necessary.
Thus:
@IBDesignable
class BubbleView: UIView {
@IBInspectable var lineWidth: CGFloat = 1 { didSet { setNeedsDisplay() } }
@IBInspectable var cornerRadius: CGFloat = 10 { didSet { setNeedsDisplay() } }
@IBInspectable var calloutSize: CGFloat = 5 { didSet { setNeedsDisplay() } }
@IBInspectable var fillColor: UIColor = .yellow { didSet { setNeedsDisplay() } }
@IBInspectable var strokeColor: UIColor = .black { didSet { setNeedsDisplay() } }
override func draw(_ rect: CGRect) {
let rect = bounds.insetBy(dx: lineWidth / 2, dy: lineWidth / 2)
let path = UIBezierPath()
// lower left corner
path.move(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY - calloutSize))
path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - calloutSize - cornerRadius),
controlPoint: CGPoint(x: rect.minX, y: rect.maxY - calloutSize))
// left
path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius))
// upper left corner
path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY),
controlPoint: CGPoint(x: rect.minX, y: rect.minY))
// top
path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY))
// upper right corner
path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius),
controlPoint: CGPoint(x: rect.maxX, y: rect.minY))
// right
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - calloutSize - cornerRadius))
// lower right corner
path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - calloutSize),
controlPoint: CGPoint(x: rect.maxX, y: rect.maxY - calloutSize))
// bottom (including callout)
path.addLine(to: CGPoint(x: rect.midX + calloutSize, y: rect.maxY - calloutSize))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX - calloutSize, y: rect.maxY - calloutSize))
path.close()
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.lineWidth = lineWidth
path.stroke()
}
}
That yields:
I think what you're doing is overly complicated. UIBezierPath gives you UIBezierPath(roundedRect:)
so why not use it? Stroke the rounded rectangle; erase the spot where you're going to put the little triangle; add the triangle; fill the compound path; and stroke the missing two sides of the triangle. Like this (this is just some code I happened to have lying around - you should change the numbers to fit your shape, of course):
let con = UIGraphicsGetCurrentContext()
CGContextTranslateCTM(con, 10, 10)
UIColor.blueColor().setStroke()
UIColor.blueColor().colorWithAlphaComponent(0.4).setFill()
let p = UIBezierPath(roundedRect: CGRectMake(0,0,250,180), cornerRadius: 10)
p.stroke()
CGContextClearRect(con, CGRectMake(20,170,10,11))
let pts = [
CGPointMake(20,180), CGPointMake(20,200),
CGPointMake(20,200), CGPointMake(30,180)
]
p.moveToPoint(pts[0])
p.addLineToPoint(pts[1])
p.addLineToPoint(pts[3])
p.fill()
CGContextStrokeLineSegments(con, pts, 4)