SwiftUI Charts中如何区分水平滚动与放大手势避免冲突?
Swift Charts中缩放与水平滚动手势冲突的解决方案
问题描述
我正在使用Swift Charts开发SwiftUI图表界面,已通过MagnifyGesture实现了基于X轴的缩放功能,但该手势与图表的水平滚动功能存在识别冲突。垂直方向执行放大手势时识别效果较好,水平方向则易与滚动操作混淆。当禁用图表交互时,放大手势可正常工作,但会丢失水平滚动功能,手动实现该功能会增加交互逻辑复杂度。
现有实现代码
import Charts import SwiftUI @MainActor struct GraphScreen: View { @Environment(ThemeManager.self) private var theme @Environment(DataModel.self) private var dataModel: DataModel @State private var scrollPosition = Date() @State private var selectedPrice: CandlestickDataPoint? @State var investmentData: [CandlestickDataPoint] = [] @State var minimum: Double = 0 @State var maximum: Double = 1 @State var minimumAdjusting: Double = 0.995 @State var maximumAdjusting: Double = 1.005 @State private var currentZoom = 0.0 @State private var totalZoom = 24.0 @State private var strideMinuteCount = 15 @State private var barWidth: CGFloat = 10 var body: some View { content .task { do { let response = try await dataModel.userStock() investmentData = ChartManager.shared.process(response: response) totalZoom = 12 } catch { print(error.localizedDescription) } } .onChange(of: totalZoom, { oldValue, newValue in // Adjust the barWidth and strideMinuteCount based on the zoom level strideMinuteCount = Int(5 * newValue) // Adjust the stride count based on some formula barWidth = max(1, CGFloat(48 / newValue)) // Adjust the bar width let visibleData = getVisibleValues() setMinMax(forValues: visibleData) }) } func getVisibleValues() -> [CandlestickDataPoint] { let minDate = scrollPosition let maxDate = scrollPosition.addingTimeInterval((currentZoom + totalZoom) * 1800) let values = investmentData.filter({ $0.date >= minDate && $0.date <= maxDate }) return values } func setMinMax(forValues values: [CandlestickDataPoint]) { minimum = (values.map({ $0.low }).min() ?? 0) //* minimumAdjusting maximum = (values.map({ $0.high }).max() ?? 1) //* maximumAdjusting } } extension GraphScreen { var content: some View { ZStack { theme.selected.backgroundScreenColor .ignoresSafeArea() VStack { chart } .padding(.bottom) } } var magnifyGesture: some Gesture { return MagnifyGesture() .onChanged { value in currentZoom = 1 - value.magnification } .onEnded { value in totalZoom += currentZoom currentZoom = 0 totalZoom = min(max(totalZoom, 1), 24) } } var chart: some View { Chart(investmentData) { item in BarMark( x: .value("Date", item.date), yStart: .value("Open", min(item.open, item.close)), yEnd: .value("Close", max(item.open, item.close)), width: .fixed(barWidth) ) .foregroundStyle(item.close > item.open ? Color.green : Color.red) RuleMark( x: .value("Date", item.date), yStart: .value("Low", item.low), yEnd: .value("High", item.high) ) .foregroundStyle(item.close > item.open ? Color.green : Color.red) } .chartYScale(domain: [minimum, maximum]) .chartYAxis { AxisMarks(position: .automatic, values: .automatic) { AxisGridLine() .foregroundStyle(theme.selected.bodyTextGrayColor) AxisValueLabel() .foregroundStyle(theme.selected.bodyTextGrayColor) } } .chartXAxis { AxisMarks(values: .stride(by: .minute, count: strideMinuteCount)) { AxisGridLine() .foregroundStyle(theme.selected.bodyTextGrayColor) AxisValueLabel() .foregroundStyle(theme.selected.bodyTextGrayColor) } } //.disabled(true) .chartScrollableAxes([.horizontal]) .chartScrollPosition(x: $scrollPosition) .onChange(of: scrollPosition) { print(scrollPosition) let visibleData = getVisibleValues() setMinMax(forValues: visibleData) } .chartXVisibleDomain(length: (currentZoom + totalZoom) * 1800) .chartGesture { chartProxy in magnifyGesture } } }
解决方案
1. 利用手势优先级与条件识别
通过给缩放手势添加优先级判断,或设置缩放响应的阈值,减少与滚动操作的误判:
var magnifyGesture: some Gesture { MagnifyGesture() .onChanged { value in // 设置缩放阈值,只有缩放变化超过一定程度才响应 let scaleDelta = abs(value.magnification - 1.0) if scaleDelta > 0.01 { currentZoom = 1 - value.magnification } } .onEnded { value in totalZoom += currentZoom currentZoom = 0 totalZoom = min(max(totalZoom, 1), 24) } // 给拖动手势更高优先级,避免滚动时触发缩放 .highPriorityGesture(DragGesture(minimumDistance: 5) .onChanged { _ in currentZoom = 0 }) }
2. 使用Swift Charts内置缩放功能(推荐)
Swift Charts提供了官方的chartZoomable修饰符,可自动处理缩放与滚动的手势冲突,无需自定义MagnifyGesture:
var chart: some View { Chart(investmentData) { item in // 保留原有Mark代码 BarMark( x: .value("Date", item.date), yStart: .value("Open", min(item.open, item.close)), yEnd: .value("Close", max(item.open, item.close)), width: .fixed(barWidth) ) .foregroundStyle(item.close > item.open ? Color.green : Color.red) RuleMark( x: .value("Date", item.date), yStart: .value("Low", item.low), yEnd: .value("High", item.high) ) .foregroundStyle(item.close > item.open ? Color.green : Color.red) } .chartYScale(domain: [minimum, maximum]) .chartYAxis { // 保留原有Y轴配置 AxisMarks(position: .automatic, values: .automatic) { AxisGridLine() .foregroundStyle(theme.selected.bodyTextGrayColor) AxisValueLabel() .foregroundStyle(theme.selected.bodyTextGrayColor) } } .chartXAxis { // 保留原有X轴配置 AxisMarks(values: .stride(by: .minute, count: strideMinuteCount)) { AxisGridLine() .foregroundStyle(theme.selected.bodyTextGrayColor) AxisValueLabel() .foregroundStyle(theme.selected.bodyTextGrayColor) } } .chartScrollableAxes([.horizontal]) .chartScrollPosition(x: $scrollPosition) .onChange(of: scrollPosition) { let visibleData = getVisibleValues() setMinMax(forValues: visibleData) } // 启用内置水平缩放,替换自定义手势 .chartZoomable(axes: [.horizontal]) { proxy, scale in // 映射内置缩放比例到原有totalZoom变量 totalZoom = 24 / scale strideMinuteCount = Int(5 * totalZoom) barWidth = max(1, CGFloat(48 / totalZoom)) let visibleData = getVisibleValues() setMinMax(forValues: visibleData) } }
3. 自定义手势识别区域
将缩放手势限制在图表的特定区域(如Y轴侧边),滚动操作保留在图表主体:
var chart: some View { HStack(spacing: 0) { // 左侧缩放触发区域 Color.clear .frame(width: 40) .gesture(magnifyGesture) // 右侧滚动区域 Chart(investmentData) { item in // 保留原有Mark代码 } .chartScrollableAxes([.horizontal]) .chartScrollPosition(x: $scrollPosition) // 保留其他图表配置 } }
内容的提问来源于stack exchange,提问作者Libor Zapletal
相关产品推荐
相关产品推荐

