You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 07:36:05