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

SwiftUI柱状图:如何实现单点击直接切换选中柱状条?

修改SwiftUI柱状图点击选中逻辑,实现直接切换选中项

问题分析

原代码通过isTapped布尔变量控制选中状态,导致点击新柱状条时会先触发取消操作,再重新选中,需要两次点击才能切换目标。核心问题是冗余的状态变量干扰了选中逻辑,我们可以直接通过currentDataID的状态来管理选中项。

修改方案

  1. 移除冗余状态变量:删除isTapped,直接用currentDataID是否为空判断是否有选中项。
  2. 重写点击手势逻辑:在CardView的点击事件中,直接对比当前选中ID和点击条的ID:
    • 若相同:清空currentDataID取消选中
    • 若不同:将currentDataID设为点击条的ID,直接切换选中
  3. 更新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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.18 11:27:59