Unfortunately, there is no LineCap style that provides this kind of effect for free.
I would suggest, the best way to solve the problem is to bite the bullet and create a custom arc shape. The benefits of doing it this way:
- The shape can be filled in all the usual ways.
- The shape outline can be stroked, for example, to show the border in a different color.
- The shape can also be used for masking, for clipping or as the content shape for a button.
Some aspects of this approach are discussed below.
Gaps between segments
When drawing the outer and inner arcs, the angles need to take the gap between segments into consideration.
In your screenshot, you will notice that the gap between the segments has a constant width. In other words, it is not a V shape.
In order that the gap remains constant, the adjustment to the arc angle will depend on the radius. As a close approximation, the adjustment can be computed as a fraction of the circle circumference, as follows:
gap angle in radians = (gap width / circumference) * 2 * pi
The circumference of a circle is given by radius * 2 * pi. So the factor 2 * pi cancels out and the angle in radians can be computed simply as gap width / radius.
Building the path
The diagram below illustrates how the path of the shape can be built by combining six separate arcs:

The arcs can be drawn using two different addArc functions:
The points at the ends of the blue arcs need to be computed by adjusting the start and end angles, to take the corner radius into consideration. This can be done in exactly the same way as the adjustment for the gap between segments, as explained above.
The custom shape
Based on the diagram and explanation above, here is an example implementation of a custom arc shape:
struct RoundedArcShape: Shape {
let startAngle: Angle
let endAngle: Angle
let arcWidth: CGFloat
let gapWidth: CGFloat
var cornerRadius: CGFloat = 6
var centered: Bool = false
private func pointOnArc(center: CGPoint, radius: CGFloat, angle: Angle) -> CGPoint {
let x = center.x + (radius * cos(angle.radians))
let y = center.y + (radius * sin(angle.radians))
return CGPoint(x: x, y: y)
}
func path(in rect: CGRect) -> Path {
let center = CGPoint(x: rect.midX, y: rect.midY)
let outerRadius = min(rect.width, rect.height) / 2
let outerGapAngle = Angle(radians: gapWidth / outerRadius)
let outerStartAngle = startAngle + (outerGapAngle / 2)
let outerEndAngle = endAngle - (outerGapAngle / 2)
let outerCornerAdjustment = Angle(radians: cornerRadius / outerRadius)
let pointA = pointOnArc(center: center, radius: outerRadius, angle: outerStartAngle)
let pointB = pointOnArc(center: center, radius: outerRadius, angle: outerStartAngle + outerCornerAdjustment)
let pointD = pointOnArc(center: center, radius: outerRadius, angle: outerEndAngle)
let innerRadius = outerRadius - arcWidth
let innerGapAngle = Angle(radians: gapWidth / innerRadius)
let innerStartAngle = startAngle + (innerGapAngle / 2)
let innerEndAngle = endAngle - (innerGapAngle / 2)
let innerCornerAdjustment = Angle(radians: cornerRadius / innerRadius)
let pointE = pointOnArc(center: center, radius: innerRadius, angle: innerEndAngle)
let pointF = pointOnArc(center: center, radius: innerRadius, angle: innerEndAngle - innerCornerAdjustment)
let pointH = pointOnArc(center: center, radius: innerRadius, angle: innerStartAngle)
let pointZ = CGPoint(
x: pointA.x + (pointH.x - pointA.x) / 2,
y: pointA.y + (pointH.y - pointA.y) / 2
)
let path = Path { path in
path.move(to: pointZ)
path.addArc(
tangent1End: pointA,
tangent2End: pointB,
radius: cornerRadius
)
path.addArc(
center: center,
radius: outerRadius,
startAngle: outerStartAngle + outerCornerAdjustment, // B
endAngle: outerEndAngle - outerCornerAdjustment, // C
clockwise: false
)
path.addArc(
tangent1End: pointD,
tangent2End: pointE,
radius: cornerRadius
)
path.addArc(
tangent1End: pointE,
tangent2End: pointF,
radius: cornerRadius
)
path.addArc(
center: center,
radius: innerRadius,
startAngle: innerEndAngle - innerCornerAdjustment, // F
endAngle: innerStartAngle + innerCornerAdjustment, // G
clockwise: true
)
path.addArc(
tangent1End: pointH,
tangent2End: pointA,
radius: cornerRadius
)
path.closeSubpath()
}
// Shift the path to the middle of the area, if required
let result: Path
if centered {
let midAngle = startAngle + ((endAngle - startAngle) / 2)
let midRadius = outerRadius - (arcWidth / 2)
let midArcPoint = pointOnArc(center: center, radius: midRadius, angle: midAngle)
result = path.offsetBy(dx: center.x - midArcPoint.x, dy: center.y - midArcPoint.y)
} else {
result = path
}
return result
}
}
You will notice that this implementation includes the option to shift the path to the middle of the drawing area. This can be useful when the shape is used as a button, see this answer for an example of where this is the case.
Using the shape
Here is an adapted version of your example to show how the custom shape can be used. A ZStack is used to combine the three separate segments, as you were doing before.
struct ContentView: View {
let arcWidth: CGFloat = 12
let gapWidth: CGFloat = 1.5
let cornerRadius: CGFloat = 3
let bgColor = Color(red: 0.15, green: 0.15, blue: 0.37)
let lightSelect = Color(red: 0.63, green: 0.88, blue: 0.92)
let mediumSelect = Color(red: 0.35, green: 0.37, blue: 0.55)
let darkSelect = Color(red: 0.23, green: 0.26, blue: 0.47)
let fraction1 = 0.53
let fraction2 = 0.22
var body: some View {
let angle0 = Angle.degrees(-90)
let angle1 = angle0 + .degrees(fraction1 * 360)
let angle2 = angle1 + .degrees(fraction2 * 360)
ZStack {
RoundedArcShape(
startAngle: angle0,
endAngle: angle1,
arcWidth: arcWidth,
gapWidth: gapWidth,
cornerRadius: cornerRadius
)
.fill(lightSelect)
RoundedArcShape(
startAngle: angle1,
endAngle: angle2,
arcWidth: arcWidth,
gapWidth: gapWidth,
cornerRadius: cornerRadius
)
.fill(mediumSelect)
RoundedArcShape(
startAngle: angle2,
endAngle: angle0,
arcWidth: arcWidth,
gapWidth: gapWidth,
cornerRadius: cornerRadius
)
.fill(darkSelect)
}
.frame(width: 100, height: 100)
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(bgColor)
}
}
