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 🥰