I am drawing a semicircular BMI gauge in a PDF utilizing UIGraphicsPDFRenderer and UIBezierPath. The coloured arc and labels render accurately, however the arrow pointer (which ought to level to the arc place akin to the BMI worth) seems within the flawed place/aspect or with the flawed rotation for the BMI I go.
I pasted the complete perform beneath. The arc seems to be superb, however after I name generateBMIGaugePDF(bmi: someValue) the arrow doesn’t sit underneath (or level precisely to) the proper location on the arc — it could be offset, mirrored, or rotated incorrectly.
What am I doing flawed? How ought to I compute the arrow place and rotation so the arrow at all times sits underneath the arc on the right radial place and factors to the arc location representing the BMI?
Please discover my perform beneath.
func generateBMIGaugePDF(
bmi: Double,
bmiMin: Double = 10,
bmiMax: Double = 40,
pageSize: CGSize = CGSize(width: 595, peak: 842)
) -> URL? {
let clampedBMI = max(min(bmi, bmiMax), bmiMin)
let pdfMeta: [String: Any] = [
kCGPDFContextAuthor as String: "YourApp",
kCGPDFContextTitle as String: "BMI Gauge"
]
let format = UIGraphicsPDFRendererFormat()
format.documentInfo = pdfMeta
let renderer = UIGraphicsPDFRenderer(bounds: CGRect(origin: .zero, dimension: pageSize), format: format)
let filename = "bmi_gauge_(Int(Date().timeIntervalSince1970)).pdf"
let tempURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(filename)
let margin: CGFloat = 40
let middle = CGPoint(x: pageSize.width / 2, y: pageSize.peak / 2 - 30)
let radius: CGFloat = min(pageSize.width - 2 * margin, pageSize.peak / 2) / 2
let lineWidth: CGFloat = 28
// I draw the semicircle from proper (0) to left (Ï€)
let startAngle = CGFloat(0)
let endAngle = CGFloat.pi
let totalAngle = endAngle - startAngle
let segments = 3
let segmentAngle = totalAngle / CGFloat(segments)
let segmentColors: [UIColor] = [
UIColor(red: 1.00, green: 0.56, blue: 0.20, alpha: 1.0),
UIColor(red: 0.18, green: 0.86, blue: 0.40, alpha: 1.0),
UIColor(red: 0.18, green: 0.55, blue: 0.98, alpha: 1.0)
]
// angleForBMI - NOTE: I believe this mapping could also be associated to the bug
func angleForBMI(_ v: Double) -> CGFloat {
let denom = bmiMax - bmiMin != 0 ? bmiMax - bmiMin : 1.0
let t = CGFloat((v - bmiMin) / denom) // normalized 0..1
// at present returning startAngle - t * totalAngle
return startAngle - t * totalAngle
}
do {
attempt renderer.writePDF(to: tempURL, withActions: { (context) in
context.beginPage()
let ctx = context.cgContext
ctx.saveGState()
ctx.setFillColor(UIColor.white.cgColor)
ctx.fill(CGRect(origin: .zero, dimension: pageSize))
// draw 3 coloured arc segments
for i in 0..<segments {
let segStart = startAngle - CGFloat(i) * segmentAngle
let segEnd = segStart - segmentAngle
let arcPath = UIBezierPath(arcCenter: middle,
radius: radius,
startAngle: segEnd,
endAngle: segStart,
clockwise: true)
arcPath.lineWidth = lineWidth
arcPath.lineCapStyle = .butt
ctx.setStrokeColor(segmentColors[i % segmentColors.count].cgColor)
ctx.setLineWidth(lineWidth)
ctx.addPath(arcPath.cgPath)
ctx.strokePath()
}
// define, labels, min/max, middle textual content ... (omitted for brevity on this paste)
// Draw arrow (pointer) beneath arc
let pointerAngle = angleForBMI(clampedBMI) // computed angle on arc (radians)
let pointerLength: CGFloat = radius * 0.9
// I initially set the arrow base like this:
let arrowBaseCenter = CGPoint(x: middle.x, y: middle.y + radius + 40)
// Then I attempted to rotate the arrow like this:
ctx.saveGState()
// <-- I attempted this and variants (translate y/3, translate middle, rotate +pi, and so on.)
ctx.translateBy(x: arrowBaseCenter.x, y: arrowBaseCenter.y / 3)
let initialArrowAngle: CGFloat = -CGFloat.pi / 2
ctx.rotate(by: pointerAngle - initialArrowAngle)
// Draw stem
let stemWidth: CGFloat = 6
let stemHeight: CGFloat = 28
let stemRect = CGRect(x: -stemWidth / 2, y: 0, width: stemWidth, peak: stemHeight)
ctx.setFillColor(UIColor.black.cgColor)
ctx.addRect(stemRect)
ctx.drawPath(utilizing: .fill)
// Draw triangle tip (tip at unfavorable Y)
let trianglePath = UIBezierPath()
trianglePath.transfer(to: CGPoint(x: 0, y: -pointerLength * 0.18))
trianglePath.addLine(to: CGPoint(x: -12, y: 0))
trianglePath.addLine(to: CGPoint(x: 12, y: 0))
trianglePath.shut()
ctx.setFillColor(UIColor.black.cgColor)
ctx.addPath(trianglePath.cgPath)
ctx.drawPath(utilizing: .fill)
ctx.restoreGState()
ctx.restoreGState()
})
return tempURL
} catch {
print("Didn't generate BMI gauge PDF: (error)")
return nil
}
}
Noticed behaviour
The arc and coloured segments draw accurately and persistently.
The arrow angle generally matches (i.e. the arrow visually rotates) however the arrow base is misplaced (typically on the alternative aspect or shifted), so it doesn’t level to the precise place on the arc akin to the BMI worth.
I’ve tried fast fixes like altering translate coordinates (arrowBaseCenter.y / 3 vs arrowBaseCenter.y), including + .pi to the rotation, altering initialArrowAngle, and so on. Nothing reliably positions the arrow on the right x/y for given BMI values.
What I count on
For any BMI worth (inside vary), the arrow ought to:
be drawn with its base situated simply outdoors the arc on the radial place that corresponds to that BMI (so horizontal/vertical place relies on the BMI),
and be rotated so its tip factors exactly to the arc level akin to the BMI.
What I attempted
Altering ctx.translateBy values.
ctx.rotate(by: pointerAngle – CGFloat.pi/2) and variants like including + .pi.
Calculating arrow base utilizing middle.x + cos(pointerAngle) * one thing — however I could not get the rotation & placement to align reliably.
Clamping BMI values — not related to the geometry however achieved.
i would like such a consequence

