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

SwiftUI Chart饼图:实现点击扇区选择而非长按触发详情浮层

SwiftUI Chart饼图实现点击扇区显示浮层+外部点击关闭

核心解决方案

要实现点击触发扇区选中,同时解决外部点击关闭浮层的问题,可从以下两点入手:

  • 用SpatialTapGesture替换默认长按交互,在chartGesture中处理点击位置对应的角度,完成选中/取消逻辑
  • 给图表的父容器添加高优先级点击手势,重置选中状态实现浮层关闭

完整代码实现

假设你的数据模型如下:

struct PieChartItem: Identifiable {
    let id = UUID()
    var label: String?
    var value: Double?
    var color: Color?
}

@State private var pieChartData: [PieChartItem] = [
    PieChartItem(label: "A", value: 30, color: .red),
    PieChartItem(label: "B", value: 50, color: .blue),
    PieChartItem(label: "C", value: 20, color: .green)
]
@State private var selectedAngle: Double?
@State private var selectedItem: PieChartItem?

修改后的图表代码:

// 父容器包裹,用于添加全局点击关闭手势
ZStack {
    Chart {
        ForEach(pieChartData.indices, id: \.self) { index in
            SectorMark(
                angle: .value(pieChartData[index].label ?? "", pieChartData[index].value ?? 0.0),
                innerRadius: (pieChartData[index].label == selectedItem?.label) ? .ratio(0.5) : .ratio(0.6),
                outerRadius: (pieChartData[index].label == selectedItem?.label) ? .ratio(1.0) : .ratio(0.9),
                angularInset: 0.4
            )
            .foregroundStyle(by: .value("Key", pieChartData[index].label ?? ""))
        }
    }
    .chartForegroundStyleScale(
        domain: pieChartData.map { $0.label ?? "" },
        range: pieChartData.map { $0.color ?? Color.random() }
    )
    .chartAngleSelection(value: $selectedAngle)
    .chartLegend(alignment: .center, spacing: 16)
    .chartGesture { chartProxy in
        SpatialTapGesture()
            .onEnded { event in
                let tappedAngle = chartProxy.angle(at: event.location)
                // 点击已选中扇区则取消选中,否则选中对应扇区
                if let currentAngle = selectedAngle, abs(tappedAngle - currentAngle) < 10 {
                    selectedAngle = nil
                    selectedItem = nil
                } else {
                    chartProxy.selectAngleValue(at: tappedAngle)
                    updateSelectedItem(from: tappedAngle)
                }
            }
    }
    .frame(height: 300)
    .overlay {
        if let item = selectedItem {
            VStack {
                Text(item.label ?? "")
                    .foregroundColor(.white)
                Text("\(item.value ?? 0)")
                    .foregroundColor(.white)
            }
            .padding(8)
            .background(RoundedRectangle(cornerRadius: 6).fill(.black.opacity(0.7)))
        }
    }
}
// 高优先级手势确保优先触发,实现外部点击关闭
.highPriorityGesture(
    TapGesture()
        .onEnded {
            selectedAngle = nil
            selectedItem = nil
        }
)

关键逻辑解释

  1. 点击交互替换:

    • 通过chartGesture绑定SpatialTapGesture,获取点击位置的角度后调用chartProxy.selectAngleValue(at:)完成选中
    • 增加点击已选中扇区取消选中的逻辑,优化交互体验
  2. 外部点击关闭:

    • 给父容器ZStack添加.highPriorityGesture,确保该手势优先于图表内部手势触发,重置selectedAngle和selectedItem即可关闭浮层
  3. 选中项匹配:
    实现updateSelectedItem(from:)方法,根据角度计算对应的数据项:

    private func updateSelectedItem(from angle: Double) {
        var cumulativeAngle = 0.0
        let totalValue = pieChartData.reduce(0.0) { $0 + ($1.value ?? 0) }
        
        for item in pieChartData {
            guard let value = item.value else { continue }
            let itemAngle = (value / totalValue) * 360
            // 转换为0-360范围的角度,避免负数判断问题
            let normalizedAngle = angle < 0 ? angle + 360 : angle
            let angleRange = cumulativeAngle..<cumulativeAngle + itemAngle
            
            if angleRange.contains(normalizedAngle) {
                selectedItem = item
                break
            }
            cumulativeAngle += itemAngle
        }
    }
    

注意事项

  • 确保PieChartItem遵循Identifiable,或在ForEach中明确指定id
  • 角度计算需处理负数情况(SwiftUI Chart的角度范围为-180到180,转换为0-360更易判断)
  • 父容器的高优先级手势要包裹整个图表区域,避免点击空白区域不触发的情况

内容的提问来源于stack exchange,提问作者RP89

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 17:38:14