Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Scrollable Tappable Chart in Swiftui

I am developing an app in SwiftUI and I currently have a chart with vertically stacked bars on it. When the user clicks on a bar a subview pops up where they can alter some values of the bar. To accomplish this I have been using GeometryReader to detect where the user clicks.

```
.chartOverlay { proxy in         
    GeometryReader { geometry in              
        Rectangle().fill(.clear).contentShape(Rectangle())                  
            .onTapGesture { location in                   
            // Get the x and y value from the location.                   
            let (min, name) = proxy.value(at: location, as: (Int, String).self) ?? (0, "")
            print("Location: \(min), \(name)")              
        }          
    }      
}
```

However the user can also add bars and when they add enough the chart becomes scrollable and GeometryReader no longer seems to work because it assumes that the position of the bars never changes. Is there a way that I can make the bars tappable and act in the same way as they would with GeometryReader while being in a scrollable chart. Thanks.

I tried making it so that it generated new GeometryReaders for each individual bar, however that would just go out of the bounds of the chart and did not work. I also tried putting those GeometryReaders in a scrollview but then the chart could no longer be scrolled itself.

like image 821
Cooper H Avatar asked Oct 26 '25 14:10

Cooper H


1 Answers

Instead of using geometry readers and invisible rectangles to implement the gesture, just use chartGesture.

.chartGesture { proxy in
    SpatialTapGesture().onEnded { value in
        let location = value.location
        // find the x and y of the tapped location
        if let (name, tappedY) = proxy.value(at: value.location, as: (String, Int).self),
           // find the item that corresponds to the bar we tapped
           let tappedIndex = data.firstIndex(where: { $0.name == name }),
           // ensure that the tapped location is below the top of the bar
           tappedY <= data[tappedIndex].y {
            // selectedIndex is a @State
            selectedIndex = tappedIndex
            print("Location: \(data[tappedIndex].y), \(name)")
        } else {
            // tapping outside of a bar would deselect it
            selectedIndex = nil
        }
    }
}

Here is a complete example, where selecting a bar would cause a Stepper to show up that allows you to change the bar's height.

struct Sample: Identifiable, Hashable {
    let id = UUID()
    let name: String
    var y: Int
}

struct ContentView: View {
    
    @State var selectedIndex: Int?
    
    @State var data = (1...20).map {
        Sample(name: "Name \($0)", y: .random(in: 1...30))
    }
    
    var body: some View {
        Chart(data) { sample in
            BarMark(
                x: .value("Name", sample.name),
                y: .value("Y", sample.y),
                width: 50
            )
        }
        .chartScrollableAxes(.horizontal)
        .chartGesture { proxy in
            SpatialTapGesture().onEnded { value in
                let location = value.location
                if let (name, tappedY) = proxy.value(at: value.location, as: (String, Int).self),
                   let tappedIndex = data.firstIndex(where: { $0.name == name }),
                   tappedY <= data[tappedIndex].y {
                    selectedIndex = tappedIndex
                    print("Location: \(data[tappedIndex].y), \(name)")
                } else {
                    selectedIndex = nil
                }
            }
        }
        .chartOverlay(alignment: .bottom) { _ in
            if let selectedIndex {
                Stepper(data[selectedIndex].name, value: $data[selectedIndex].y)
                    .padding()
                    .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 10))
                    .padding()
            }
        }
    }
}
like image 130
Sweeper Avatar answered Oct 29 '25 05:10

Sweeper



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!