I’d write this as a Form
that’s crammed, as an alternative of a Path
that’s stroked.
The trail of the Form
is the union
of:
- a path created by the
strokedPath
of a (trimmed) round path, with the.butt
line cap. - a round path with the radius of the road width, at the place the trimmed path ends.
struct OneEndRoundedCircle: Form {
var trim: CGFloat
let lineWidth: CGFloat
func path(in rect: CGRect) -> Path {
let radius = min(rect.width, rect.top) / 2
let middle = CGPoint(x: rect.midX, y: rect.midY)
let circlePath = Path(
ellipseIn: CGRect(origin: middle, dimension: .zero)
.insetBy(dx: -radius, dy: -radius)
).trimmedPath(from: 0, to: trim)
let stroked = circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
// that is the place we wish the rounded finish to be.
// In case your path begins some other place, modify the angle accordingly
let roundedPoint = middle.making use of(.init(
translationX: radius * sin(.pi / 2),
y: radius * cos(.pi / 2)
))
let littleCircle = Path(
ellipseIn: .init(origin: roundedPoint, dimension: .zero)
.insetBy(dx: -lineWidth / 2, dy: -lineWidth / 2)
)
return stroked.union(littleCircle)
}
}
// Animatable conformance in case you wish to animate the quantity trimmed
extension OneEndRoundedCircle: Animatable {
var animatableData: CGFloat {
get { trim }
set { trim = newValue }
}
}
non-public func speedProgressView(width: CGFloat) -> some View {
ZStack {
// deliberately elevated lineWidth to make the rounded finish extra seen
OneEndRoundedCircle(trim: 0.2, lineWidth: 10)
.fill(.crimson)
.shadow(coloration: .inexperienced, radius: 10, x: 0, y: 0)
.shadow(coloration: .inexperienced, radius: 2, x: 0, y: 0)
}
.body(width: width)
.rotationEffect(.levels(100))
}
Instance output:
For iOS 17 or earlier, union
isn’t out there, however you possibly can simply work with CGPath
s as an alternative, and use its union
methodology as an alternative.
To realize the outcome within the screenshot, the place each ends are rounded when the speedometer is full, you possibly can simply examine the worth of trim
and return
early.
// assuming you wish to depart the underside 20 levels of the circle all the time empty,
// the speedometer is full when the trim ends at 17/18
if abs(trim - 17 / 18) < 0.00001 {
return circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .spherical))
}
let stroked = circlePath.strokedPath(StrokeStyle(lineWidth: lineWidth, lineCap: .butt))
// add the little circle like earlier than...