SwiftUI柱状图:如何实现单点击直接切换选中柱状条?
修改SwiftUI柱状图点击选中逻辑,实现直接切换选中项
问题分析
原代码通过isTapped布尔变量控制选中状态,导致点击新柱状条时会先触发取消操作,再重新选中,需要两次点击才能切换目标。核心问题是冗余的状态变量干扰了选中逻辑,我们可以直接通过currentDataID的状态来管理选中项。
修改方案
- 移除冗余状态变量:删除
isTapped,直接用currentDataID是否为空判断是否有选中项。 - 重写点击手势逻辑:在
CardView的点击事件中,直接对比当前选中ID和点击条的ID:- 若相同:清空
currentDataID取消选中 - 若不同:将
currentDataID设为点击条的ID,直接切换选中
- 若相同:清空
- 更新UI判断条件:把原代码中依赖
isTapped的UI状态(透明度、文字颜色),替换为!currentDataID.isEmpty。
修改后的完整代码
struct BarGraph: View { var exampleData: [ExampleData] // Gesture Properties... @GestureState var isDragging: Bool = false @State var offset: CGFloat = 0 // Current download to highlight while dragging or tapping... @State var currentDataID: String = "" // 移除冗余的isTapped状态变量 var body: some View { VStack(spacing: 80) { HStack(spacing: 10) { ForEach(exampleData) { data in CardView(data: data) // 移除外层重复的onTapGesture,避免和CardView内部手势冲突 } } .frame(height: 150) .animation(.easeOut, value: isDragging) // Gesture... .gesture( DragGesture() .updating($isDragging, body: { _, out, _ in out = true }) .onChanged({ value in // Only updating if dragging... offset = isDragging ? value.location.x : 0 // Dragging space removing the padding added to the view... // Total padding = 60 // 2 * 15 Horizontal let draggingSpace = UIScreen.main.bounds.width - 60 // Each block... let eachBlock = draggingSpace / CGFloat(exampleData.count) // Getting index... let temp = Int(offset / eachBlock) // Safe wrapping index... let index = max(min(temp, exampleData.count - 1), 0) // Updating ID self.currentDataID = exampleData[index].id }) .onEnded({ value in withAnimation { offset = .zero currentDataID = "" } }) ) RoundedRectangle(cornerRadius: 10) .fill(Color("BackGround")) .frame(width: 160, height: 80) .overlay( VStack(spacing: 8) { Text("Average") .foregroundColor(.black) .font(.system(size: UIScreen.main.bounds.size.width * 0.065)) .fontWeight(.semibold) Text("\(getDataNumber())") .foregroundColor(.black) .font(.system(size: UIScreen.main.bounds.size.width * 0.065)) } ) } } func getXPosition() -> CGFloat { let index = exampleData.firstIndex { $0.id == currentDataID } ?? 0 let barWidth: CGFloat = 30 let spacing: CGFloat = 10 let initialOffset: CGFloat = (UIScreen.main.bounds.width - CGFloat(exampleData.count) * (barWidth + spacing) + spacing) / 2 return initialOffset + CGFloat(index) * (barWidth + spacing) + barWidth / 2 } func getDataNumber() -> Int { if let data = exampleData.first(where: { $0.id == currentDataID }) { return Int(data.number) } return 0 } @ViewBuilder func CardView(data: ExampleData) -> some View { VStack(spacing: 20) { GeometryReader { proxy in let size = proxy.size RoundedRectangle(cornerRadius: 6) .fill(data.color) // 用!currentDataID.isEmpty替代isTapped,判断是否处于选中状态 .opacity(isDragging || !currentDataID.isEmpty ? (currentDataID == data.id ? 1 : 0.35) : 1) .frame(height: (data.number / getMax()) * (size.height)) .overlay( Text("") .opacity(isDragging && currentDataID == data.id ? 1 : 0) // 替换isTapped为!currentDataID.isEmpty .opacity(!currentDataID.isEmpty && currentDataID == data.id ? 1 : 0) .offset(y: -30), alignment: .top ) .frame(maxHeight: .infinity, alignment: .bottom) } Text(data.day) .font(.callout) // 替换isTapped为!currentDataID.isEmpty .foregroundColor((isDragging || !currentDataID.isEmpty) && currentDataID == data.id ? data.color : .gray) } .onTapGesture { // 重写点击逻辑,直接基于currentDataID判断 if currentDataID == data.id { // 点击已选中的条,取消选中 currentDataID = "" } else { // 点击未选中的条,直接切换选中 currentDataID = data.id } } } func getMax() -> CGFloat { let max = exampleData.max { first, second in return second.number > first.number } return max?.number ?? 0 } }
效果说明
修改后:
- 点击任意未选中的柱状条,会直接切换为选中状态,其余条透明度降低
- 点击已选中的柱状条,会取消选中,所有条恢复正常透明度
- 拖拽交互的原有逻辑保持不变
内容的提问来源于stack exchange,提问作者Mxyb
相关产品推荐
相关产品推荐

