I try to add circular dashed border on an UIView.
I wanna make each dash with white color and stroked with black color, such as the line in the image: Styled dash line
I had tried to use 2 CAShapeLayers to draw the border, but they are always not matched with each other.
let rect = CGRect(origin: .zero, size: .init(width:180, height:180))
let path = UIBezierPath(ovalIn: rect).cgPath
let dashedBorderLayer = CAShapeLayer()
dashedBorderLayer.strokeColor = UIColor.black.cgColor
dashedBorderLayer.fillColor = nil
dashedBorderLayer.lineDashPattern = [4, 2]
dashedBorderLayer.lineWidth = 3
dashedBorderLayer.path = path
self.view.layer.addSublayer(dashedBorderLayer)
let dashedBorderLayer2 = CAShapeLayer()
dashedBorderLayer2.strokeColor = UIColor.white.cgColor
dashedBorderLayer2.fillColor = nil
dashedBorderLayer2.lineDashPattern = [2, 4]
dashedBorderLayer2.lineWidth = 2
dashedBorderLayer2.path = path
self.view.layer.addSublayer(dashedBorderLayer2)
Suppose the border you want is represented by a path p
, the idea is to create a path q
such that when it is filled, the pixels drawn will be the same as if p
were stroked with dashes. Then you can use q
as the path
of a shape layer.
// suppose our border is a 150x150 square
let p = CGPath(rect: .init(x: 0, y: 0, width: 150, height: 150), transform: nil)
let q = dashStrokedPath(p)
let shapeLayer = CAShapeLayer()
shapeLayer.path = q
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.fillColor = UIColor.white.cgColor
shapeLayer.lineWidth = 1
The dashStrokedPath
function converts p
into q
. This can be implemented using a combination of copy(dashingWithPhase:lengths:)
and copy(strokingWithWidth:lineCap:lineJoin:miterLimit:)
:
func dashStrokedPath(_ path: CGPath) -> CGPath {
// adjust the parameters here as you wish...
let strokedAndDashed = path.copy(dashingWithPhase: 5, lengths: [10, 20])
.copy(strokingWithWidth: 5, lineCap: .round, lineJoin: .round, miterLimit: 10)
return CGPath(rect: strokedAndDashed.boundingBox, transform: nil).intersection(
strokedAndDashed
)
}
Note that as the last step, I intersected the path with its own bounding box. This is to remove what looks like self-intersecting lines. Without the intersection, it looks like this:
With the intersection, it looks like this:
Before iOS 16, intersection
is not available. You can instead hide the self-intersection by putting a masked layer on top of the layer with the stroke., though this will also hide half of the stroke width, i.e. the stroke will only be visible on the "outside" of the path.
let p = CGPath(rect: .init(x: 20, y: 20, width: 150, height: 150), transform: nil)
// dashStrokedPath will just return the path returned by the two 'copy' calls, without 'intersection'
let q = dashStrokedPath(p)
let shapeLayer = CAShapeLayer()
shapeLayer.path = q
shapeLayer.strokeColor = UIColor.black.cgColor
shapeLayer.lineWidth = 2
// 'v' is a UIView
v.layer.addSublayer(shapeLayer)
let layerMask = CAShapeLayer()
layerMask.path = q
let maskedLayer = CAShapeLayer()
maskedLayer.path = CGPath(rect: q.boundingBox, transform: nil)
maskedLayer.fillColor = UIColor.white.cgColor
maskedLayer.mask = layerMask
v.layer.addSublayer(maskedLayer)