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.
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()
}
}
}
}