Search code examples
iosuikitcashapelayerswift5

Modify endcap .round style, to be more apparently round, unlike the example image shown here


I've just learned that the standard Apple .round endcap

let l = CAShapeLayer()
l.path = .. just a straight line, say 50 long
l.lineWidth =  say "3.1" in the example shown here
l.lineCap = .round

is not very apparently round in some cases, see images.

If I want apparently "exact semi-circle" endcaps, what to do?

I appreciate that I could build my own line line and fill it.

But is there a way to perhaps subclass "endcap" in some way?

It would be magic if you could say = .myHappyEndcapMode

enter image description here

How do we fix this?

enter image description here

enter image description here

enter image description here

You can see how far off it is here:

enter image description here

Further info:

The non-round visual look appears to arise in the case of:

  • Thin lines
  • Possibly when the width is non-integer
  • In the situation at hand I had gradients on gradients and fades everywhere, it may not help

Solution

  • There is no way to “subclass” the cap shape. You will have to manually construct the outline of the shape if you want a different cap shape.

    The round cap is already exactly as circular as a circle created with CGPath(ellipseIn:).

    Here's a round-capped line drawn on top of a circle of slightly larger radius, zoomed 8x:

    zoomed image

    The curvature of the line cap starts exactly at the horizontal center of the circle.

    Here's the code:

    import UIKit
    import PlaygroundSupport
    
    let lineWidth: CGFloat = 20
    let circleRadius = lineWidth / 2 + 1
    
    let lineLayer = CAShapeLayer()
    lineLayer.position = .init(x: 30, y: 30)
    let path = CGMutablePath()
    path.move(to: .zero)
    path.addLine(to: .init(x: 30, y: 0))
    lineLayer.path = path
    lineLayer.lineWidth = lineWidth
    lineLayer.lineCap = .round
    lineLayer.strokeColor = UIColor.gray.cgColor
    lineLayer.fillColor = nil
    
    let circleLayer = CAShapeLayer()
    circleLayer.position = lineLayer.position
    circleLayer.path = CGPath(ellipseIn: CGRect.zero.insetBy(dx: -circleRadius, dy: -circleRadius), transform: nil)
    circleLayer.fillColor = UIColor.blue.cgColor
    
    let view = UIView(frame: .init(x: 0, y: 0, width: 100, height: 80))
    view.layer.backgroundColor = UIColor.white.cgColor
    view.layer.addSublayer(circleLayer)
    view.layer.addSublayer(lineLayer)
    PlaygroundPage.current.liveView = view