Make dynamic dashed border in Swift: avoid lumping and watch original cat memes
Let's imagine you need to add a dashed border to a view. Normally, you would write code like that.
First iteration
private func addUglyDashedBorder() {
borderLayer = CAShapeLayer()
let viewRect = CGRect(x: 0, y: 0, width: frame.size.width, height: frame.size.height)
borderLayer.bounds = viewRect
borderLayer.position = CGPoint(x: frame.size.width / 2, y: frame.size.height / 2)
borderLayer.fillColor = UIColor.clear.cgColor
borderLayer.strokeColor = UIColor.black.cgColor
borderLayer.lineWidth = 1
borderLayer.lineJoin = .round
borderLayer.lineDashPattern = [5, 5]
borderLayer.path = UIBezierPath(roundedRect: viewRect, cornerRadius: Constant.Dimens.cornerRadius).cgPath // Constant.Dimens.cornerRadius == 12
layer.addSublayer(borderLayer)
}
Result
Looks ok. But what is this ugly artefact in the top left corner? Looks like the dashes are too close.
Cat meme #1
The reason
The reason for that is plain. iOS creates a path and then follows the pattern: draw a line for 5 points, leave 5 more points empty. It goes on till it fills the entire path. But what happens if the path length is for example 106 points? Then we will see 10 complete segments – meaning they have 5 filled and 5 empty points. It gives us 100 points. Then iOS draws another dash – we are now at 105 points. For the last empty space, there is only 1 point left! It looks like two dashes almost merged. Rather ugly.
One could get lucky. Such a not dynamic approach can work. But it does not mean it will always work – we have phones of different sizes. On one of them, there might be a configuration that looks ugly. So we have to fix it. And I have a feeling that we cannot find a magic number that fits all ;)
Cat meme #2
Better approach
private func addDashedBorder() {
// all the same
borderLayer.lineDashPattern = [dashLength(), dashLength()]
// still the same code
}
private func dashLength() -> NSNumber {
let perimeterWithoutRoundedCorners = frame.size.width * 2 + frame.size.height * 2 - 8 * Constant.Dimens.cornerRadius
let cornersPerimeter = (2 * CGFloat.pi * Constant.Dimens.cornerRadius)
let perimeter = perimeterWithoutRoundedCorners + cornersPerimeter // 1
let desiredLengthPerDashAndSpacing: CGFloat = 10
let amountOfSegments = perimeter / desiredLengthPerDashAndSpacing // 2
let roundedAmountOfSegments = round(amountOfSegments) // 3
let segmentLength = perimeter / roundedAmountOfSegments // 4
return NSNumber(value: segmentLength / 2) // 5
}
In human-readable words:
- Get the view's perimeter. My view has rounded corners, thus extra calculus;
- Calculate how many segments of dash + spacing there are. It is a fractional number;
- Round the count of segments: we want to have the count of segments whole;
- Divide perimeter by new count of segments to learn how long a segment should be;
- Remember, a segment is a dash and spacing sequence. If they have the same length we have to divide them in half.
🎉
Cat meme #3
Thank you for watching this cat frenzy – and checking out the article. I wish your house to always be full of moderately fat cats