Swift - Cut hole in shadow layer - ios

I want to "cut a hole" in the shadow layer of a UIView an Swift3, iOS
I have a container (UIView), that has 2 children:
one UIImageView
one UIView on top of that image ("overlay")
I want to give the overlay a shadow and cut out an inner rect of that shadow, to create a glow-like effect at the edges of the ImageView
It is crucial that the glow is inset, since the image is taking the screen width
My code so far:
let glowView = UIView(frame: CGRect(x: 0, y: 0, width: imageWidth, height: imageHeight))
glowView.layer.shadowPath = UIBezierPath(roundedRect: container.bounds, cornerRadius: 4.0).cgPath
glowView.layer.shouldRasterize = true
glowView.layer.rasterizationScale = UIScreen.main.scale
glowView.layer.shadowOffset = CGSize(width: 1.0, height: 1.0)
glowView.layer.shadowOpacity = 0.4
container.addSubview(imageView)
container.addSubview(glowView)
The result looks like the following right now:
Now I would like to cut out the darker inner part, so that just the shadow at the edges remains
Any idea how to achieve this?

Fortunately it's now very easy today (2020)
These days it's very easy to do this:
Here's the whole thing
import UIKit
class GlowBox: UIView {
override func layoutSubviews() {
super.layoutSubviews()
backgroundColor = .clear
layer.shadowOpacity = 1
layer.shadowColor = UIColor.red.cgColor
layer.shadowOffset = CGSize(width: 0, height: 0)
layer.shadowRadius = 3
let p = UIBezierPath(
roundedRect: bounds.insetBy(dx: 0, dy: 0),
cornerRadius: 4)
let hole = UIBezierPath(
roundedRect: bounds.insetBy(dx: 2, dy: 2),
cornerRadius: 3)
.reversing()
p.append(hole)
layer.shadowPath = p.cgPath
}
}
A really handy tip:
When you add (that is .append ) two bezier paths like that...
The second one has to be either "normal" or "reversed".
Notice the line of code near the end:
.reversing()
Like most programmers, I can NEVER REMEMBER if it should be "normal" or "reversed" in different cases!!
The simple solution ...
Very simply, try both!
Simply try it with, and without, the .reversing()
It will work one way or the other! :)
If you want JUST the shadow to be ONLY outside, with the insides exactly cut out:
override func layoutSubviews() {
super.layoutSubviews()
// take EXTREME CARE of frame vs. bounds
// and + vs - throughout this function:
let _rad: CGRect = 42
colorAndShadow.frame = bounds
colorAndShadow.path =
UIBezierPath(roundedRect: bounds, cornerRadius: _rad).cgPath
let enuff: CGFloat = 200
shadowHole.frame = colorAndShadow.frame.insetBy(dx: -enuff, dy: -enuff)
let _sb = shadowHole.bounds
let p = UIBezierPath(rect: _sb)
let h = UIBezierPath(
roundedRect: _sb.insetBy(dx: enuff, dy: enuff),
cornerRadius: _rad)
p.append(h)
shadowHole.fillRule = .evenOdd
shadowHole.path = p.cgPath
layer.mask = shadowHole
}
shadowHole is a CAShapeLayer that you must set up using a lazy variable in the usual way.

Try using this as as your shadow path:
let shadowWidth = 2.0 // Do this as wide as you want
var outterPath = UIBezierPath()
outterPath.move(to: CGPoint(x: shadowWidth, y: 0))
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width, y: 0))
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width, y: glowView.bounds.size.height))
outterPath.addLine(to: CGPoint(x: 0.0, y: glowView.bounds.size.height))
outterPath.addLine(to: CGPoint(x: 0.0, y: 0.0))
outterPath.addLine(to: CGPoint(x: shadowWidth, y: 0.0))
outterPath.addLine(to: CGPoint(x: shadowWidth, y: glowView.bounds.size.height - shadowWidth))
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width - shadowWidth, y: glowView.bounds.size.height - shadowWidth))
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width - shadowWidth, y: shadowWidth))
outterPath.addLine(to: CGPoint(x: shadowWidth, y: shadowWidth))
outterPath.close()
This won't create a rounded rect but with a little bit of changes to the code above you should be able to add those too.

Thanks to Mihai Fratu's answer I was able to create a UIBezierPath that exactly meets my needs
I post my code here in case somebody has the same problem later
let innerRadius: CGFloat = 32.0 * UIScreen.main.scale
let shadowPath: UIBezierPath = UIBezierPath(roundedRect: self.view.bounds, cornerRadius: self.cornerRadius)
//shadowPath.append(UIBezierPath(roundedRect: self.view.bounds.insetBy(dx: 8, dy: 8), cornerRadius: self.cornerRadius))
let shadowWidth: CGFloat = 8.0 // Do this as wide as you want
var outterPath = UIBezierPath()
// Start at the top left corner with an x offset of the cornerRadius
outterPath.move(to: CGPoint(x: self.cornerRadius, y: 0))
// Draw a line to the top right corner
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width - self.cornerRadius, y: 0))
//Draw the round top right corner
outterPath.addArc(withCenter: CGPoint(x: glowView.bounds.size.width - self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: (3 * CGFloat.pi) / 2, endAngle: 0, clockwise: true)
// Draw a line to the bottom right corner
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width, y: glowView.bounds.size.height - self.cornerRadius))
// Draw the round bottom right corner
outterPath.addArc(withCenter: CGPoint(x: glowView.bounds.size.width - self.cornerRadius, y: glowView.bounds.size.height - self.cornerRadius), radius: self.cornerRadius, startAngle: 0, endAngle: CGFloat.pi / 2, clockwise: true)
// Draw a line to the bottom left corner
outterPath.addLine(to: CGPoint(x: self.cornerRadius, y: glowView.bounds.size.height))
// Draw the round bottom left corner
outterPath.addArc(withCenter: CGPoint(x: self.cornerRadius, y: glowView.bounds.size.height - self.cornerRadius), radius: self.cornerRadius, startAngle: CGFloat.pi / 2, endAngle: CGFloat.pi, clockwise: true)
// Draw a line to the top left corner
outterPath.addLine(to: CGPoint(x: 0.0, y: self.cornerRadius))
// Draw the round top left corner
outterPath.addArc(withCenter: CGPoint(x: self.cornerRadius, y: self.cornerRadius), radius: self.cornerRadius, startAngle: CGFloat.pi, endAngle: (3 * CGFloat.pi) / 2, clockwise: true)
// Move to the inner start point and add the paths counterclockwise to prevent the filling of the inner area
outterPath.move(to: CGPoint(x: shadowWidth + innerRadius, y: shadowWidth))
// Draw the inner top left corner
outterPath.addArc(withCenter: CGPoint(x: shadowWidth + innerRadius, y: shadowWidth + innerRadius), radius: innerRadius, startAngle: (3 * CGFloat.pi) / 2, endAngle: CGFloat.pi, clockwise: false)
// Draw a line to the inner bottom left corner
outterPath.addLine(to: CGPoint(x: shadowWidth, y: glowView.bounds.size.height - innerRadius - shadowWidth))
// Draw the inner bottom left corner
outterPath.addArc(withCenter: CGPoint(x: shadowWidth + innerRadius, y: glowView.bounds.size.height - innerRadius - shadowWidth), radius: innerRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi / 2, clockwise: false)
// Draw a line to the inner bottom right corner
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width - innerRadius - shadowWidth, y: glowView.bounds.size.height - shadowWidth))
// Draw the inner bottom right corner
outterPath.addArc(withCenter: CGPoint(x: glowView.bounds.size.width - innerRadius - shadowWidth, y: glowView.bounds.size.height - innerRadius - shadowWidth), radius: innerRadius, startAngle: CGFloat.pi / 2, endAngle: 0, clockwise: false)
// Draw a line to the inner top right corner
outterPath.addLine(to: CGPoint(x: glowView.bounds.size.width - shadowWidth, y: shadowWidth + innerRadius))
// Draw the inner top right corner
outterPath.addArc(withCenter: CGPoint(x: glowView.bounds.size.width - innerRadius - shadowWidth, y: shadowWidth + innerRadius), radius: innerRadius, startAngle: 0, endAngle: (3 * CGFloat.pi) / 2, clockwise: false)
// Draw a line to the inner top left corner
outterPath.addLine(to: CGPoint(x: shadowWidth + innerRadius, y: shadowWidth))
outterPath.close()

Related

Inverted Rounded Rectangle Swift

How would I draw an inverted rounded rectangle as shown below in Swift using a UIBezierPath? To clarify, I want to ONLY draw the shape in black.
While most of us would simply put a white view with rounded corners on top of the black view (it is simpler and it more accurately reflects the visual result), if you really want to draw that shape, create a UIBezierPath that consists of the upper left arc, the upper right arc, and the add lines to the two bottom corners. E.g.
let path = UIBezierPath(
arcCenter: CGPoint(x: rect.minX + cornerRadius, y: rect.minY),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi / 2,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY),
radius: cornerRadius,
startAngle: .pi / 2,
endAngle: 0,
clockwise: false
)
path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY))
path.close()
There are a ton of ways to render this:
Create CAShapeLayer with this path and use that as a mask (which I show below);
Add this CAShapeLayer as a sublayer of the view’s layer;
Create a UIImage using UIGraphicsImageRenderer, and fill that UIBezierPath;
etc.
For example:
#IBDesignable class BottomView: UIView {
#IBInspectable var cornerRadius: CGFloat = 15 { didSet { setNeedsLayout() } }
override func layoutSubviews() {
super.layoutSubviews()
let path = UIBezierPath(
arcCenter: CGPoint(x: bounds.minX + cornerRadius, y: bounds.minY),
radius: cornerRadius,
startAngle: .pi,
endAngle: .pi / 2,
clockwise: false
)
path.addArc(
withCenter: CGPoint(x: bounds.maxX - cornerRadius, y: bounds.minY),
radius: cornerRadius,
startAngle: .pi / 2,
endAngle: 0,
clockwise: false
)
path.addLine(to: CGPoint(x: bounds.maxX, y: bounds.maxY))
path.addLine(to: CGPoint(x: bounds.minX, y: bounds.maxY))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
shapeLayer.fillColor = UIColor.white.cgColor // the color here is irrelevant; only used to define the mask
layer.mask = shapeLayer
}
}
That yields the following when I (a) add this BottomView to my view hierarchy; and (b) set the backgroundColor of this view to .black (or whatever you want):
I made this #IBDesignable so that I could either add it in Interface Builder storyboard, but you can also add it programmatically, too.
Or you can use this UIBezierPath with any of the patterns enumerated above. Whatever works for you.
What I do is draw two views: the black rectangle, and the top region view with the curves (which I call a "roundyThingy"). So the only interesting part is how to draw the top region view. It too is just a black rectangle, but it has a mask that cuts out the roundy part. So the only really interesting part is the mask:
let size = roundyThingy.bounds.size
let renderer = UIGraphicsImageRenderer(size: size)
let image = renderer.image { context in
UIColor.black.setFill()
context.fill(.init(origin: .zero, size: size))
let path = UIBezierPath(
roundedRect: .init(origin: .zero, size: size),
byRoundingCorners: [.bottomLeft, .bottomRight],
cornerRadii: .init(width: 12, height: 12)
)
path.fill(with: .clear, alpha: 1)
}
roundyThingy.mask = UIImageView(image: image)
roundyThingy.mask?.frame = roundyThingy.bounds

How to add Curve left and right side in UIView for swift

I tried to add a curve but not working.
I have attached the image left side is given by the designer, the right side I have done in swift.
here the if I add corner radius not working for the curve if add curve corner radius not working.
I have added left side and right side separate views, how it's possible to achieve the remove the super view background-color and curve.
extension UIView {
func roundCorners(
corners: UIRectCorner,
radius: CGFloat
) {
let path = UIBezierPath(
roundedRect: bounds,
byRoundingCorners: corners,
cornerRadii: CGSize(
width: radius,
height: radius
)
)
let mask = CAShapeLayer()
mask.path = path.cgPath
layer.mask = mask
}
}
// here I am using on UITableviewCell in layoutSubview
leftCurve.roundCorners(corners: [.topRight,.bottomRight], radius: 20)
rightCurver.roundCorners(corners: [.topLeft,.bottomLeft], radius: 20)
Here is a demo for add curve to both side using UIBezierPath
class ShapeView: UIView {
var path: UIBezierPath = UIBezierPath()
// Set you circle position for verticle.
var circleYPosition: CGFloat = 50 {
didSet {
self.setNeedsDisplay()
}
}
override func awakeFromNib() {
super.awakeFromNib()
self.backgroundColor = UIColor.clear
// Here apply shadow
}
override func draw(_ rect: CGRect) {
super.draw(rect)
// 4 corners radious
UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 10, height: 10)).addClip()
// Left - right circle
path.move(to: CGPoint(x: 0, y: self.frame.size.height))
//left side
path.addArc(withCenter: CGPoint(x: 0, y: self.circleYPosition - 15),
radius: 10,
startAngle: CGFloat((90 * Double.pi) / 180),
endAngle: CGFloat((270 * Double.pi) / 180),
clockwise: false)
path.addLine(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: self.frame.size.width, y: 0))
path.addLine(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
path.move(to: CGPoint(x: self.frame.size.width, y: self.frame.size.height))
//right side
path.addArc(withCenter: CGPoint(x: self.frame.size.width, y: self.circleYPosition - 15),
radius: 10,
startAngle: CGFloat((270 * Double.pi) / 180),
endAngle: CGFloat((90 * Double.pi) / 180),
clockwise: false)
path.close()
UIColor.white.setFill()
path.fill()
// Center Dash path
let dashPath = UIBezierPath()
dashPath.move(to: CGPoint(x: self.bounds.minX + 12, y: self.circleYPosition - 15))
dashPath.addLine(to: CGPoint(x: self.bounds.maxX - 12, y: self.circleYPosition - 15))
dashPath.setLineDash([5,5], count: 2, phase: 0.0)
dashPath.lineWidth = 1.0
dashPath.lineCapStyle = .butt
UIColor.lightGray.set()
dashPath.stroke()
}
}

how to make cropped UIView to show the bottom of that UIView?

I need to make something like this
as you can see, there are two half round views in the right and left side that will show the bottom UIView( UIView with dark blue background color).how to achieve this?
I simplify the problem to be like the picture below:
what should I do to that white view to make cropped effect and actually show the background view (blue view) ?
edit: no, I can't give blue color to that white view. as you can see in the first picture, the background color is actually gradient color, thats why i need to "crop" this UIView to show the background color of the bottom UIView
Here is the view that you want to achieve. The goal is that you need to draw this view with corners and arcs. If you need any help or explanation about how I did that you can just ask.
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .blue
view.addSubview(croppedView)
croppedView.cutViewCornersWith(cornerRadius: 20, arcRadius: 14)
}
lazy var croppedView: CroppedView = {
let cv = CroppedView(frame: CGRect(x: 0,
y: 0,
width: self.view.frame.width - 120,
height: 400))
cv.center = view.center
cv.backgroundColor = .lightGray
return cv
}()
}
import UIKit
class CroppedView: UIView {
func cutViewCornersWith(cornerRadius: CGFloat, arcRadius: CGFloat) {
let path = UIBezierPath()
let width = self.frame.width
let height = self.frame.height
let arcCenter = height - height/3
path.move(to: CGPoint(x: 0, y: cornerRadius))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(180.0).toRadians(),
endAngle: CGFloat(270.0).toRadians(),
clockwise: true)
path.addLine(to: CGPoint(x: width - cornerRadius, y: 0.0))
path.addArc(withCenter: CGPoint(x: width - cornerRadius, y: cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(90.0).toRadians(),
endAngle: CGFloat(0.0).toRadians(),
clockwise: true)
path.addLine(to: CGPoint(x: width, y: arcCenter - arcRadius))
path.addArc(withCenter: CGPoint(x: width, y: arcCenter),
radius: arcRadius,
startAngle: CGFloat(270.0).toRadians(),
endAngle: CGFloat(90.0).toRadians(),
clockwise: false)
path.addLine(to: CGPoint(x: width, y: height - cornerRadius))
path.addArc(withCenter: CGPoint(x: width - cornerRadius, y: height - cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(0.0).toRadians(),
endAngle: CGFloat(90.0).toRadians(),
clockwise: true)
path.addLine(to: CGPoint(x: cornerRadius, y: height))
path.addArc(withCenter: CGPoint(x: cornerRadius, y: height - cornerRadius),
radius: cornerRadius,
startAngle: CGFloat(90.0).toRadians(),
endAngle: CGFloat(180.0).toRadians(),
clockwise: true)
path.addLine(to: CGPoint(x: 0, y: arcCenter + arcRadius))
path.addArc(withCenter: CGPoint(x: 0, y: arcCenter),
radius: arcRadius,
startAngle: CGFloat(90.0).toRadians(),
endAngle: CGFloat(270.0).toRadians(),
clockwise: false)
path.addLine(to: CGPoint(x: 0, y: arcCenter - arcRadius))
path.addLine(to: CGPoint(x: 0, y: 0))
path.close()
let shapeLayer = CAShapeLayer()
shapeLayer.path = path.cgPath
self.layer.mask = shapeLayer
}
}
extension CGFloat {
func toRadians() -> CGFloat {
return self * .pi / 180.0
}
}
There is a way to do this in code. You need to set a 'mask'. I am not sure if you can do this using the InterfaceBuilder (I stay well clear of it). Here is the gist of the code that I used (cut from my code, but untested). Here I make a transparent strip the width of the view and holeHeight tall - I was custom drawing the middle of a picker.
class HoledView: UIView {
override func draw(_ rect: CGRect) {
backgroundColor?.setFill()
UIRectFill(rect)
let layer = CAShapeLayer()
let path = CGMutablePath()
let rect = CGRect(x: 0, y: (self.frame.height - holeHeight) / 2, width: self.frame.width, height: holeHeight)
path.addRect(rect)
path.addRect(self.bounds)
layer.path = path
layer.fillRule = kCAFillRuleEvenOdd
self.layer.mask = layer
}
}
The basic idea is that the mask defines the area that the view will draw into. In my example there is a hole in the middle of the mask, so the view will not draw into that area, and it stays completely transparent.
Although I did this using layers, it may be doable using a mask view instead:
The view’s alpha channel determines how much of the view’s content and background shows through. Fully or partially opaque pixels allow the underlying content to show through but fully transparent pixels block that content.

Some visual bug with CALayer

I have a UITableView with the following cells layout:
• UIView
•• UIImageView
••• Semi-transparent overlay UIView as subview of UIImageView
There are also icon view imitating the "play" button and UILabel, but they seem to be not related to this bug.
And I have the following code:
override func layoutSubviews() {
super.layoutSubviews()
createPath()
}
private func createPath() {
let padding = CGFloat(16)
let bottomLineLength = CGFloat(64)
let arcPadding = CGFloat(8)
let width = self.contentView.frame.width
let height = self.contentView.frame.height
if pathLayer != nil {
pathLayer?.removeFromSuperlayer()
}
pathLayer = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: padding + arcPadding, y: padding))
path.addLine(to: CGPoint(x: width - padding - arcPadding, y: padding))
// Top-Right arc
path.addArc(withCenter: CGPoint(x: width - padding - arcPadding, y: padding + arcPadding),
radius: arcPadding,
startAngle: CGFloat(3 * M_PI / 2),
endAngle: CGFloat(M_PI * 2),
clockwise: true)
path.move(to: CGPoint(x: width - padding, y: padding + arcPadding))
path.addLine(to: CGPoint(x: width - padding, y: height - padding - arcPadding))
// Bottom-Right arc
path.addArc(withCenter: CGPoint(x: width - padding - arcPadding, y: height - padding - arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI * 2),
endAngle: CGFloat(M_PI / 2),
clockwise: true)
path.move(to: CGPoint(x: width - padding - arcPadding, y: height - padding))
path.addLine(to: CGPoint(x: width - padding - bottomLineLength, y: height - padding))
path.move(to: CGPoint(x: padding + bottomLineLength, y: height - padding))
path.addLine(to: CGPoint(x: padding + arcPadding, y: height - padding))
// Bottom-Left arc
path.addArc(withCenter: CGPoint(x: padding + arcPadding, y: height - padding - arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI / 2),
endAngle: CGFloat(M_PI),
clockwise: true)
path.move(to: CGPoint(x: padding, y: height - padding - arcPadding))
path.addLine(to: CGPoint(x: padding, y: padding + arcPadding))
// Top-Left arc
path.addArc(withCenter: CGPoint(x: padding + arcPadding, y: padding + arcPadding),
radius: arcPadding,
startAngle: CGFloat(M_PI),
endAngle: CGFloat(3 * M_PI / 2),
clockwise: true)
pathLayer?.rasterizationScale = 2 * UIScreen.main.scale
pathLayer?.shouldRasterize = true
pathLayer?.strokeColor = UIColor(red: 0xFF/255, green: 0xFF/255, blue: 0xFF/255, alpha: 0.5).cgColor
pathLayer?.lineWidth = 2.0
pathLayer?.path = path.cgPath
contentView.layer.addSublayer(pathLayer!)
}
I'm calling it in layoutSubviews for re-creating the path on device rotation.
And if you look at the screenshot, there are some black and rotated a bit black "holes" near the path. All of them look the same, and they are surely not present on the image in UIImageView.
I think it's related to my CALayer. How to fix it?
Your problem is these black wedges:
The underlying cause is that the default fillColor of a CAShapeLayer is opaque black. Set it to nil to avoid filling:
pathLayer?.fillColor = nil
That done, let me show you a shorter way to construct your path:
let cgPath = CGMutablePath()
let start = CGPoint(x: padding + bottomLineLength, y: height - padding)
let blCorner = CGPoint(x: padding, y: height - padding)
let tlCorner = CGPoint(x: padding, y: padding)
let trCorner = CGPoint(x: width - padding, y: padding)
let brCorner = CGPoint(x: width - padding, y: height - padding)
let end = CGPoint(x: width - padding - bottomLineLength, y: height - padding)
cgPath.move(to: start)
cgPath.addArc(tangent1End: blCorner, tangent2End: tlCorner, radius: arcPadding)
cgPath.addArc(tangent1End: tlCorner, tangent2End: trCorner, radius: arcPadding)
cgPath.addArc(tangent1End: trCorner, tangent2End: brCorner, radius: arcPadding)
cgPath.addArc(tangent1End: brCorner, tangent2End: end, radius: arcPadding)
cgPath.addLine(to: end)
shapeLayer.path = cgPath
Result:

How to add rounded corner to a UIBezierPath custom rectangle?

I managed to create the rounded corners, but I'm having trouble with the first rounded corner (lower right )
Question :
Can I add an (addArcWithCenter) method before the ( moveToPoint ) method ?
How can i get rid of the straight line at the beginning of the rectangle (lower right) ?
here is my code for the custom rectangle and a screenshot :
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 300, y: 0))
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 at the bottom
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.closePath()
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)
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 the bounds 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 the rect that is passed to this method, but rather refer to the bounds (inset, as described above). The CGRect passed to draw(_:) is the rectangle being drawn, not necessarily the full bounds. (It generally is, but not always, so always refer to the bounds of the view, not the rect 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
public class BubbleView: UIView {
#IBInspectable public var lineWidth: CGFloat = 1 { didSet { setNeedsDisplay() } }
#IBInspectable public var cornerRadius: CGFloat = 10 { didSet { setNeedsDisplay() } }
#IBInspectable public var calloutSize: CGSize = CGSize(width: 10, height: 5) { didSet { setNeedsDisplay() } }
#IBInspectable public var fillColor: UIColor = .yellow { didSet { setNeedsDisplay() } }
#IBInspectable public var strokeColor: UIColor = .black { didSet { setNeedsDisplay() } }
override public 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.height))
path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - calloutSize.height - cornerRadius),
controlPoint: CGPoint(x: rect.minX, y: rect.maxY - calloutSize.height))
// 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.height - cornerRadius))
// lower right corner
path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY - calloutSize.height),
controlPoint: CGPoint(x: rect.maxX, y: rect.maxY - calloutSize.height))
// bottom (including callout)
path.addLine(to: CGPoint(x: rect.midX + calloutSize.width / 2, y: rect.maxY - calloutSize.height))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.addLine(to: CGPoint(x: rect.midX - calloutSize.width / 2, y: rect.maxY - calloutSize.height))
path.close()
fillColor.setFill()
path.fill()
strokeColor.setStroke()
path.lineWidth = lineWidth
path.stroke()
}
}
That yields:
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()
Swift 5 with configuration variables:
override func draw(_ rect: CGRect) {
let arrowXOffset: CGFloat = 13
let cornerRadius: CGFloat = 6
let arrowHeight: CGFloat = 6
let mainRect = CGRect(origin: rect.origin, size: CGSize(width: rect.width, height: rect.height - arrowHeight))
let leftTopPoint = mainRect.origin
let rightTopPoint = CGPoint(x: mainRect.maxX, y: mainRect.minY)
let rightBottomPoint = CGPoint(x: mainRect.maxX, y: mainRect.maxY)
let leftBottomPoint = CGPoint(x: mainRect.minX, y: mainRect.maxY)
let leftArrowPoint = CGPoint(x: leftBottomPoint.x + arrowXOffset, y: leftBottomPoint.y)
let centerArrowPoint = CGPoint(x: leftArrowPoint.x + arrowHeight, y: leftArrowPoint.y + arrowHeight)
let rightArrowPoint = CGPoint(x: leftArrowPoint.x + 2 * arrowHeight, y: leftArrowPoint.y)
let path = UIBezierPath()
path.addArc(withCenter: CGPoint(x: rightTopPoint.x - cornerRadius, y: rightTopPoint.y + cornerRadius), radius: cornerRadius,
startAngle: CGFloat(3 * Double.pi / 2), endAngle: CGFloat(2 * Double.pi), clockwise: true)
path.addArc(withCenter: CGPoint(x: rightBottomPoint.x - cornerRadius, y: rightBottomPoint.y - cornerRadius), radius: cornerRadius,
startAngle: 0, endAngle: CGFloat(Double.pi / 2), clockwise: true)
path.addLine(to: rightArrowPoint)
path.addLine(to: centerArrowPoint)
path.addLine(to: leftArrowPoint)
path.addArc(withCenter: CGPoint(x: leftBottomPoint.x + cornerRadius, y: leftBottomPoint.y - cornerRadius), radius: cornerRadius,
startAngle: CGFloat(Double.pi / 2), endAngle: CGFloat(Double.pi), clockwise: true)
path.addArc(withCenter: CGPoint(x: leftTopPoint.x + cornerRadius, y: leftTopPoint.y + cornerRadius), radius: cornerRadius,
startAngle: CGFloat(Double.pi), endAngle: CGFloat(3 * Double.pi / 2), clockwise: true)
path.addLine(to: rightTopPoint)
path.close()
}
You can't do this automatically. You have to make the lines shorter and then use arcs of the radius that you want the corner radius to be.
So. Instead of adding a line to x,y you add the line to x-radius, y.
Then add the arc. Then the next line starts at x, y+radius.

Resources