如何为UIGraphicsGetCurrentContext绘制的饼图各扇形添加点击手势
Adding Tap Gesture Detection to Your Pie Chart
Great question! Let's modify your PieChartView to support tap gestures so you can detect exactly which segment was tapped. Here's a step-by-step implementation:
Step 1: Add Gesture Recognizer & Callback Closure
First, we'll add a tap gesture recognizer to the view and a closure to notify external code when a segment is tapped. We'll also add a helper method to normalize angles (since circle calculations can wrap around the 0/2π boundary).
Step 2: Implement Tap Handling Logic
We need to:
- Check if the tap is inside the pie chart's circle
- Calculate the angle of the tap relative to the center
- Match that angle to the corresponding segment's angle range
Modified Full Code
Here's your updated PiechartView.swift with all the tap detection logic included:
import UIKit private extension CGFloat { /// Formats the CGFloat to a maximum of 1 decimal place. var formattedToOneDecimalPlace : String { let formatter = NumberFormatter() formatter.numberStyle = .decimal formatter.minimumFractionDigits = 0 formatter.maximumFractionDigits = 1 return formatter.string(from: NSNumber(value: self.native)) ?? "\(self)" } } /// Defines a segment of the pie chart struct Segment { /// The color of the segment var color : UIColor /// The name of the segment var name : String /// The value of the segment var value : CGFloat } class PieChartView: UIView { /// An array of structs representing the segments of the pie chart var segments = [Segment]() { didSet { setNeedsDisplay() } // re-draw view when the values get set } /// Defines whether the segment labels should be shown when drawing the pie chart var showSegmentLabels = true { didSet { setNeedsDisplay() } } /// Defines whether the segment labels will show the value of the segment in brackets var showSegmentValueInLabel = false { didSet { setNeedsDisplay() } } /// The font to be used on the segment labels var segmentLabelFont = UIFont.systemFont(ofSize: 20) { didSet { textAttributes[.font] = segmentLabelFont setNeedsDisplay() } } /// Callback triggered when a segment is tapped var onSegmentTapped: ((Segment) -> Void)? private let paragraphStyle : NSParagraphStyle = { var p = NSMutableParagraphStyle() p.alignment = .center return p.copy() as! NSParagraphStyle }() private lazy var textAttributes : [NSAttributedString.Key : Any] = { return [.paragraphStyle : self.paragraphStyle, .font : self.segmentLabelFont] }() override init(frame: CGRect) { super.init(frame: frame) isOpaque = false // when overriding drawRect, you must specify this to maintain transparency. setupTapGesture() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setupTapGesture() } private func setupTapGesture() { let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:))) addGestureRecognizer(tapGesture) } @objc private func handleTap(_ gesture: UITapGestureRecognizer) { let tapPoint = gesture.location(in: self) let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5) let radius = min(frame.width, frame.height) * 0.5 // Check if tap is inside the pie chart circle let dx = tapPoint.x - viewCenter.x let dy = tapPoint.y - viewCenter.y let distanceSquared = dx*dx + dy*dy guard distanceSquared <= radius*radius else { return // Ignore taps outside the pie chart } // Calculate the angle of the tap (in radians) let tapAngle = atan2(dy, dx) let normalizedTapAngle = normalizeAngle(tapAngle) // Calculate total value of all segments to determine angle ranges let totalValue = segments.reduce(0, {$0 + $1.value}) var currentStartAngle = -CGFloat.pi * 0.5 // Match the starting angle used in draw() // Iterate through segments to find which one contains the tap angle for segment in segments { let segmentAngle = 2 * .pi * (segment.value / totalValue) let currentEndAngle = currentStartAngle + segmentAngle let normalizedStart = normalizeAngle(currentStartAngle) let normalizedEnd = normalizeAngle(currentEndAngle) var isTapped = false if normalizedStart < normalizedEnd { // Segment doesn't wrap around the 0/2π boundary isTapped = normalizedTapAngle >= normalizedStart && normalizedTapAngle <= normalizedEnd } else { // Segment wraps around the 0/2π boundary (e.g., starts at 350°, ends at 10°) isTapped = normalizedTapAngle >= normalizedStart || normalizedTapAngle <= normalizedEnd } if isTapped { onSegmentTapped?(segment) break } currentStartAngle = currentEndAngle } } /// Normalizes an angle to the range [0, 2π) private func normalizeAngle(_ angle: CGFloat) -> CGFloat { var normalized = angle.truncatingRemainder(dividingBy: 2 * .pi) if normalized < 0 { normalized += 2 * .pi } return normalized } override func draw(_ rect: CGRect) { // get current context let ctx = UIGraphicsGetCurrentContext() // radius is the half the frame's width or height (whichever is smallest) let radius = min(frame.width, frame.height) * 0.5 // center of the view let viewCenter = CGPoint(x: bounds.size.width * 0.5, y: bounds.size.height * 0.5) // enumerate the total value of the segments by using reduce to sum them let valueCount = segments.reduce(0, {$0 + $1.value}) // the starting angle is -90 degrees (top of the circle, as the context is flipped). By default, 0 is the right hand side of the circle, with the positive angle being in an anti-clockwise direction (same as a unit circle in maths). var startAngle = -CGFloat.pi * 0.5 // loop through the values array for segment in segments { // set fill color to the segment color ctx?.setFillColor(segment.color.cgColor) // update the end angle of the segment let endAngle = startAngle + .pi * 2 * (segment.value / valueCount) // move to the center of the pie chart ctx?.move(to: viewCenter) // add arc from the center for each segment (anticlockwise is specified for the arc, but as the view flips the context, it will produce a clockwise arc) ctx?.addArc(center: viewCenter, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) // fill segment ctx?.fillPath() if showSegmentLabels { // do text rendering // get the angle midpoint let halfAngle = startAngle + (endAngle - startAngle) * 0.5; // the ratio of how far away from the center of the pie chart the text will appear let textPositionValue : CGFloat = 0.67 // get the 'center' of the segment. It's slightly biased to the outer edge, as it's wider. let segmentCenter = CGPoint(x: viewCenter.x + radius * textPositionValue * cos(halfAngle), y: viewCenter.y + radius * textPositionValue * sin(halfAngle)) // text to render – the segment value is formatted to 1dp if needed to be displayed. let textToRender = showSegmentValueInLabel ? "\(segment.name) (\(segment.value.formattedToOneDecimalPlace))" : segment.name // get the color components of the segement color guard let colorComponents = segment.color.cgColor.components else { return } // get the average brightness of the color let averageRGB = (colorComponents[0] + colorComponents[1] + colorComponents[2]) / 3 // if too light, use black. If too dark, use white textAttributes[.foregroundColor] = (averageRGB > 0.7) ? UIColor.black : UIColor.white // the bounds that the text will occupy var renderRect = CGRect(origin: .zero, size: textToRender.size(withAttributes: textAttributes)) // center the origin of the rect renderRect.origin = CGPoint(x: segmentCenter.x - renderRect.size.width * 0.5, y: segmentCenter.y - renderRect.size.height * 0.5) // draw text in the rect, with the given attributes textToRender.draw(in: renderRect, withAttributes: textAttributes) } // update startAngle for next segment startAngle = endAngle } } }
How to Use the Tap Detection
In your view controller (or wherever you use the pie chart), set the onSegmentTapped closure to handle tap events:
// Example setup in a ViewController override func viewDidLoad() { super.viewDidLoad() let pieChart = PieChartView(frame: CGRect(x: 50, y: 100, width: 300, height: 300)) pieChart.segments = [ Segment(color: .systemRed, name: "Red", value: 25), Segment(color: .systemBlue, name: "Blue", value: 40), Segment(color: .systemGreen, name: "Green", value: 35) ] // Handle tap events pieChart.onSegmentTapped = { tappedSegment in print("Tapped segment: \(tappedSegment.name) (value: \(tappedSegment.value))") // Add your custom logic here (e.g., show a detail view, update UI) } view.addSubview(pieChart) }
Key Notes:
- We normalize angles to handle the wrap-around at the 0/2π boundary (so segments that go from near 360° to near 0° are detected correctly)
- The tap gesture only responds to taps inside the pie chart's circle
- We reuse the same angle calculation logic from the
draw()method to ensure consistency between rendering and tap detection
内容的提问来源于stack exchange,提问作者Bappaditya
相关产品推荐
相关产品推荐

