19 C
Canberra
Sunday, November 2, 2025

Swift / CoreGraphics: semicircular arc attracts accurately however arrow pointer place/rotation does not match BMI worth in exported PDF


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

Swift / CoreGraphics: semicircular arc attracts accurately however arrow pointer place/rotation does not match BMI worth in exported PDF

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

[td_block_social_counter facebook="tagdiv" twitter="tagdivofficial" youtube="tagdiv" style="style8 td-social-boxed td-social-font-icons" tdc_css="eyJhbGwiOnsibWFyZ2luLWJvdHRvbSI6IjM4IiwiZGlzcGxheSI6IiJ9LCJwb3J0cmFpdCI6eyJtYXJnaW4tYm90dG9tIjoiMzAiLCJkaXNwbGF5IjoiIn0sInBvcnRyYWl0X21heF93aWR0aCI6MTAxOCwicG9ydHJhaXRfbWluX3dpZHRoIjo3Njh9" custom_title="Stay Connected" block_template_id="td_block_template_8" f_header_font_family="712" f_header_font_transform="uppercase" f_header_font_weight="500" f_header_font_size="17" border_color="#dd3333"]
- Advertisement -spot_img

Latest Articles