Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI path — a curve to connect two lines smoothly

I have two lines with a SwiftUI Path (see image). I want to connect these with a perfect smooth arc so it looks like the speech bubble tail in this image.

I'm struggling to get it to connect smoothly. As you can see in my image, it's almost perfect but the first connection between the line and the arc (on the right) isn't perfect.

Any ideas what I'm doing wrong? I even drew this out on paper and I can't see which value isn't correct.

enter image description here

struct SpeechBubbleTail: Shape {
    private let radius: CGFloat
    init(radius: CGFloat = 10) {
        self.radius = radius
    }

    func path(in rect: CGRect) -> Path {
        Path { path in
            path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
            path.addLine(to: CGPoint(x: rect.minX + radius, y: rect.maxY - radius))
            
            path.addArc(
                center: CGPoint(x: rect.minX + ((radius / 2) * 1.5), y: rect.maxY - (radius * 1.5)),
                            radius: (radius / 2),
                            startAngle: Angle(degrees: 45),
                            endAngle: Angle(degrees: 180),
                            clockwise: false
                        )
                        
            path.addLine(to: CGPoint(x: rect.minX + ((radius / 2) * 0.5), y: rect.minY))
        }
    }
}
like image 395
Joseph Avatar asked Sep 07 '25 08:09

Joseph


1 Answers

Let us rename radius to diameter instead, since you used radius / 2 as the radius...

So you have two lines that you want to join with a circular arc.

  • a diagonal line from (x: rect.maxX, y: rect.minY) to (x: rect.minX + diameter, y: rect.maxY - diameter), and,
  • a vertical line at x = rect.minX + diameter / 4

Observation: The centre of the circle cannot be at (x: rect.minX + ((diameter / 2) * 1.5), y: rect.maxY - (diameter * 1.5).

Demo on Desmos

enter image description here

The point highlighted is where the diagonal line ends. The circle does not go through it at all.


Don't worry though, there is a convenient addArc(tangent1End:tangent2End:radius:transform:) API that takes 2 lines and a radius, and produces a circle that is tangent to both!

path.move(to: CGPoint(x: rect.maxX, y: rect.minY))
// addArc will draw this line automatically, so you don't even need to draw it yourself
// path.addLine(to: CGPoint(x: rect.minX + diameter, y: rect.maxY - diameter))
path.addArc(
    // tangent1End is the intersection of the two lines when you extend them
    tangent1End: CGPoint(x: rect.minX + diameter / 4, y: rect.maxY - diameter / 4),
    tangent2End: CGPoint(x: rect.minX + ((diameter / 2) * 0.5), y: rect.minY),
    radius: diameter / 2)
path.addLine(to: CGPoint(x: rect.minX + ((diameter / 2) * 0.5), y: rect.minY))

This post has a very nice figure showing how this overload works.

See also this post on Math.SE for the mathematics behind this.

like image 52
Sweeper Avatar answered Sep 09 '25 18:09

Sweeper