HomeiOS DevelopmentSwift / CoreGraphics: semicircular arc attracts appropriately however arrow pointer place/rotation would...

Swift / CoreGraphics: semicircular arc attracts appropriately however arrow pointer place/rotation would 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 appropriately, however the arrow pointer (which ought to level to the arc place equivalent to the BMI worth) seems within the improper place/facet or with the improper rotation for the BMI I move.

I pasted the total operate beneath. The arc seems advantageous, however after I name generateBMIGaugePDF(bmi: someValue) the arrow doesn’t sit below (or level precisely to) the proper location on the arc — it might be offset, mirrored, or rotated incorrectly.

What am I doing improper? How ought to I compute the arrow place and rotation so the arrow all the time sits below the arc on the right radial place and factors to the arc location representing the BMI?

Please discover my operate 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, measurement: 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 the moment returning startAngle - t * totalAngle
        return startAngle - t * totalAngle
    }

    do {
        strive 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, measurement: 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 many others.)
            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 adverse 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("Did not generate BMI gauge PDF: (error)")
        return nil
    }
}

Noticed behaviour

The arc and coloured segments draw appropriately and constantly.

The arrow angle generally matches (i.e. the arrow visually rotates) however the arrow base is misplaced (typically on the other facet or shifted), so it doesn’t level to the precise place on the arc equivalent 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 many others. 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 exterior 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 equivalent 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 completed.

i need one of these consequence

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

RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

- Advertisment -
Google search engine

Most Popular

Recent Comments