SwiftUI Charts点击柱状图显示标注时柱状图位置偏移问题求助
解决SwiftUI Charts点击柱状图后位置偏移问题
问题根源
当前代码给所有柱状图都绑定了固定的annotation,无论是否选中,多余的标注会干扰图表布局导致偏移;同时嵌套在水平ScrollView中时,点击位置的计算未考虑滚动偏移,也会间接影响图表的重绘逻辑。
修复方案
1. 仅给选中的柱状图显示标注
修改BarMark的annotation逻辑,只在当前柱子匹配选中状态时才渲染标注,避免多余元素挤占布局空间:
BarMark( x: .value("Week", barMark.week), y: .value("Recalls", Double(barMark.count)) ) .foregroundStyle(Color("BorderColor")) // 仅选中的柱子显示标注 .annotation(position: .top, alignment: .center) { if barMark.week == selectedState { Text(selectedState ?? "") .padding(4) .background(Color.white) .cornerRadius(4) .shadow(radius: 2) } }
2. 修正ScrollView中的点击位置计算
由于图表嵌套在水平ScrollView中,需要跟踪滚动偏移来修正点击坐标:
首先定义偏好键跟踪滚动偏移:
private struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} }
然后给ScrollView添加偏移跟踪:
// 添加状态变量记录滚动偏移 @State private var scrollOffset: CGFloat = 0 // 修改ScrollView代码 ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { _ in GroupBox ("Recalls by state") { // 原有Chart代码保留 } .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).origin.x) } ) } } .coordinateSpace(name: "scrollView") .onPreferenceChange(ScrollOffsetKey.self) { value in scrollOffset = value }
最后更新点击处理方法,修正x坐标计算:
func updateSelectedState(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) { // 结合滚动偏移修正点击位置 let adjustedX = location.x - geometry[proxy.plotAreaFrame].origin.x - scrollOffset guard let state: String = proxy.value(atX: adjustedX) else { return } selectedState = state selectedCount = statsVM.getRecallsByState(with: coreVM.recallResults).first { $0.week == state }?.count ?? 0 }
3. 固定图表绘图区域避免重绘偏移
给Chart添加固定高度的绘图区域背景,防止标注的显示/消失触发布局重排:
Chart { // 原有ForEach和BarMark代码 } .chartPlotAreaBackground { Color.clear.frame(height: 200) // 固定绘图区域高度 }
完整修改后代码片段
// 新增偏好键 private struct ScrollOffsetKey: PreferenceKey { static var defaultValue: CGFloat = 0 static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {} } // 状态变量 @State private var selectedState: String? @State private var selectedCount: Int = 0 @State private var scrollOffset: CGFloat = 0 // 视图部分 ScrollView(.horizontal, showsIndicators: false) { ScrollViewReader { _ in GroupBox ("Recalls by state") { Chart { let recallData = statsVM.getRecallsByState(with: recallMockData) ForEach(recallData, id: \.self) { barMark in BarMark( x: .value("Week", barMark.week), y: .value("Recalls", Double(barMark.count)) ) .foregroundStyle(Color("BorderColor")) .annotation(position: .top, alignment: .center) { if barMark.week == selectedState { Text(selectedState ?? "") .padding(4) .background(Color.white) .cornerRadius(4) .shadow(radius: 2) } } } } .chartOverlay { proxy in GeometryReader { geometry in Rectangle().fill(.clear).contentShape(Rectangle()) .onTapGesture { location in updateSelectedState(at: location, proxy: proxy, geometry: geometry) } } } .chartPlotAreaBackground { Color.clear.frame(height: 200) } .padding() .chartYAxis { AxisMarks(position: .leading) } .frame(width: 450) } .background( GeometryReader { geo in Color.clear .preference(key: ScrollOffsetKey.self, value: geo.frame(in: .named("scrollView")).origin.x) } ) } } .coordinateSpace(name: "scrollView") .onPreferenceChange(ScrollOffsetKey.self) { value in scrollOffset = value } .frame(height: 250) // 点击处理方法 func updateSelectedState(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) { let adjustedX = location.x - geometry[proxy.plotAreaFrame].origin.x - scrollOffset guard let state: String = proxy.value(atX: adjustedX) else { return } selectedState = state selectedCount = statsVM.getRecallsByState(with: coreVM.recallResults).first { $0.week == state }?.count ?? 0 }
关键说明
- 仅渲染选中柱子的标注,避免多余元素干扰图表布局;
- 结合ScrollView滚动偏移修正点击位置,确保选中逻辑准确;
- 固定绘图区域高度,防止标注状态变化触发图表重绘偏移。
内容的提问来源于stack exchange,提问作者Robert
相关产品推荐
相关产品推荐

