Instead of doing the math to figure out how to draw the arc, I would just use subtracting to “subtract” the circle from a right-angled triangle.
struct Beak: Shape {
let radius: CGFloat // this is the radius of the circle to be subtracted
let angle: Angle
func path(in rect: CGRect) -> Path {
// first draw a right-angled triangle where the two short edges
// intersect at the middle of rect
var path = Path()
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addLine(to: CGPoint(x: rect.minX, y: rect.midY))
let ratio = tan(angle.radians)
let height = ratio * rect.width / 2
path.addLine(to: CGPoint(x: rect.midX, y: rect.midY - height))
path.closeSubpath()
// then create the circle
let circle = Path(ellipseIn: CGRect(
x: rect.midX - radius,
y: rect.midY - radius,
width: radius * 2,
height: radius * 2
))
return path.subtracting(circle)
}
}
Then the beak and body can be combined like this:
var body: some View {
ZStack {
// MARK: Body
Circle()
.fill(.yellow)
.overlay(alignment: .center) {
Circle()
.stroke(lineWidth: 5)
}
.frame(width: 150)
// MARK: Beak
Beak(radius: 75, angle: .degrees(16))
.stroke(style: .init(lineWidth: 5, lineJoin: .round))
.frame(width: 295, height: 295)
}
.frame(width: 300, height: 300)
.background(.orange)
}
Notes:
- I put the stroked circle above the filled circle to make it easier to reason about where the circle’s stroke is, and how thick it is, making aligning the strokes of the two paths easier.
- I used a
.roundline join to stroke the beak, because a miter join looks terrible - I made the frame of the beak slightly smaller, so that the stroke doesn’t go beyond the orange background.
Output:





