The iOS coordinate system is not the same as the two-dimensional coordinate system in our geometry textbook!

# BezierPath draws arcs

To draw an arc using UIBezierPath, you usually use addArc directly:

addArc(withCenter:, radius:, startAngle:, endAngle:, clockwise:)
Copy the code

Or use addCurve for quasi-arc:

addCurve(to:, controlPoint1:, controlPoint2:)
Copy the code

In fact, we can determine the information of this circle (radius, center) by two coordinate points (startPoint & endPoint) and the arc corresponding to the line segment between two points (Angle /radian). So can we encapsulate a function that only provides start, end, and Angle to draw arc?

addArc(startPoint: , endPoint: , angle: , clockwise:)
Copy the code

# Calculate the distance between two points

The logic here is very simple and I won’t go over it too much.

func calculateLineLength(_ point1: CGPoint, _ point2: CGPoint) -> CGFloat {
    let w = point1.x - point2.x
    let h = point1.y - point2.y
    return sqrt(w * w + h * h)
}
Copy the code

# Calculate the Angle between two points

Calculate the Angle of the Point and Origin connection in the iOS coordinate system

func calculateAngle(point: CGPoint, origin: CGPoint) -> Double {
   
    if point.y == origin.y {
        return point.x > origin.x ? 0.0 : -Double.pi
    }
    
    if point.x == origin.x {
        return point.y > origin.y ? Double.pi * 0.5 : Double.pi * -0.5
    }
    // Note: Modify the standard coordinate Angle to iOS coordinate
    let rotationAdjustment = Double.pi * 0.5
    
    let offsetX = point.x - origin.x
    let offsetY = point.y - origin.y
    // Note: -offsety is used because of the difference between iOS and standard coordinates
    if offsetY > 0 {
        return Double(atan(offsetX / -offsetY)) + rotationAdjustment
    } else {
        return Double(atan(offsetX / -offsetY)) - rotationAdjustment
    }
}
Copy the code

# Compute the coordinates of the center of the circle

If you’ve lost enough geometry, I’ve drawn a rough sketch here, as follows (Angle is small) :

When Angle is larger:

So we can write the code for computing the center point as follows

// Woring: computes only the center of a circle less than π** clockwise from start to end
// Note: Calculate counterclockwise (end to start), which can be regarded as the center of the circle when the start and end passed in is reversed
// Note: If end and start are greater than π, end and start are equal to 2π-angle
// Note: start, end, Angle
func calculateCenterFor(startPoint start: CGPoint, endPoint end: CGPoint, radian: Double) -> CGPoint {
    guard radian <= Double.pi else {
        fatalError("Does not support radian calculations greater than π!)}guardstart ! = endelse {
        fatalError("Start position and end position cannot be equal!")}if radian == Double.pi {
        let centerX = (end.x - start.x) * 0.5 + start.x
        let centerY = (end.y - start.y) * 0.5 + start.y
        return CGPoint(x: centerX, y: centerY)
    }
    
    let lineAB = calculateLineLength(start, end)
    
    // Parallel to the Y-axis
    if start.x == end.x {
        let centerY = (end.y - start.y) * 0.5 + start.y
        let tanResult = CGFloat(tan(radian * 0.5))
        let offsetX = lineAB * 0.5 / tanResult
        let centerX = start.x + offsetX * (start.y > end.y ? 1.0 : -1.0)
        return CGPoint(x: centerX, y: centerY)
    }
    
    // Parallel to the X-axis
    if start.y == end.y {
        let centerX = (end.x - start.x) * 0.5 + start.x
        let tanResult = CGFloat(tan(radian * 0.5))
        let offsetY = lineAB * 0.5 / tanResult
        let centerY = start.y + offsetY * (start.x < end.x ? 1.0 : -1.0)
        return CGPoint(x: centerX, y: centerY)
    }
    
    // Common case
    
    // Calculate the radius
    let radius = lineAB * 0.5 / CGFloat(sin(radian * 0.5))
    // Calculate the Angle with the Y-axis
    let angleToYAxis = atan(abs(start.x - end.x) / abs(start.y - end.y))
    let cacluteAngle = CGFloat(Double.pi - radian) * 0.5 - angleToYAxis
    / / the offset
    let offsetX = radius * sin(cacluteAngle)
    let offsetY = radius * cos(cacluteAngle)
    
    var centetX = end.x
    var centerY = end.y
    // Use start as the starting point to determine the quadrant interval (iOS coordinates)
    if end.x > start.x && end.y < start.y {
        // First quadrant
        centetX = end.x + offsetX
        centerY = end.y + offsetY
    } else if end.x > start.x && end.y > start.y {
        // Second quadrant
        centetX = start.x - offsetX
        centerY = start.y + offsetY
    } else if end.x < start.x && end.y > start.y {
        // Third quadrant
        centetX = end.x - offsetX
        centerY = end.y - offsetY
    } else if end.x < start.x && end.y < start.y {
        // Quadrant 4
        centetX = start.x + offsetX
        centerY = start.y - offsetY
    }
    
    return CGPoint(x: centetX, y: centerY)
}
Copy the code

Attached is a sketch of the center of the circle in the first diagram counterclockwise, where start and end have been switched

If you are confused about which calculation should use + or -, you can also draw some sketches for verification. In short, if you are confused, do 🤭

# Implement our target function

With the function that calculates the center of the circle, and the Angle between the two points we were able to easily implement addArc(startPoint -, endPoint -, Angle -,…).

func addArc(startPoint start: CGPoint, endPoint end: CGPoint, angle: Double, clockwise: Bool) {
    
    guardstart ! = end && (angle >=0 && angle <= 2 * Double.pi) else {
        return
    }
    if angle == 0 {
        move(to: start)
        addLine(to: end)
        return
    }
    
    var tmpStart = start, tmpEnd = end, tmpAngle = angle
    // Note: Ensure that the center of the circle is calculated clockwise from start to end with an Angle less than π
    if tmpAngle > Double.pi {
        tmpAngle = 2 * Double.pi - tmpAngle
        (tmpStart, tmpEnd) = (tmpEnd, tmpStart)
    }
    if! clockwise { (tmpStart, tmpEnd) = (tmpEnd, tmpStart) }let center = calculateCenterFor(startPoint: tmpStart, endPoint: tmpEnd, radian: tmpAngle)
    let radius = calculateLineLength(start, center)
    
    var startAngle = calculateAngle(point: start, origin: center)
    var endAngle = calculateAngle(point: end, origin: center)
    // Note: Counterclockwise drawing swaps startAngle and endAngle, and moves the start point to the end position
    if! clockwise { (startAngle, endAngle) = (endAngle, startAngle) move(to: end) } addArc(withCenter: center, radius: radius, startAngle:CGFloat(startAngle), endAngle: CGFloat(endAngle), clockwise: true)
    move(to: end)
}
Copy the code

# end

Finally, I do not know whether you will encounter the same requirements, here is attached source code and a sample and the running result diagram;

override func draw(_ rect: CGRect) {
    
    let path = UIBezierPath(a)var start = CGPoint(x: 160, y: 130)
    var end = CGPoint(x: 180, y: 200)
    path.move(to: start)
    path.addArc(startPoint: start, endPoint: end, angle: Double.pi * 1.6, clockwise: true)
    path.move(to: start)
    path.addArc(startPoint: start, endPoint: end, angle: Double.pi * 0.8, clockwise: true)
    
    start = CGPoint(x: 142, y: 130)
    end = CGPoint(x: 162, y: 200)
    path.move(to: start)
    path.addArc(startPoint: start, endPoint: end, angle: Double.pi * 0.4, clockwise: true)
    
    start = CGPoint(x: 140, y: 130)
    end = CGPoint(x: 160, y: 200)
    path.move(to: start)
    path.addArc(startPoint: start, endPoint: end, angle: Double.pi * 1.6, clockwise: false)
    path.move(to: start)
    path.addArc(startPoint: start, endPoint: end, angle: Double.pi * 0.8, clockwise: false)
    
    path.close()
    path.lineWidth = 1
    UIColor.red.setStroke()
    path.stroke()
}
Copy the code

Ps: Is it annoying to write Double. PI/x every time? Try an interface similar to the one provided by SwiftUI, using degress instead of radian

struct Angle {
    private var degress: Double
    static func deggess(_ degress: Double) -> Angle {
        return .init(degress: degress)
    }
    / / radian
    var radians: Double { Double.pi * degress / 180.0}}/ / Angle. Deggess (90). Radians / / 1.570796326794897
Copy the code
func addArc(startPoint start: CGPoint, endPoint end: CGPoint, angle: Angle, clockwise: Bool)
Copy the code

Thanks for reading and good luck 🥰