I’ve spent a couple hours trying to figure out how to make SwiftUI charts interactive (not static, can tap, drag, there’s a tooltip and a cursor on the selected data). I found the word “interactive” mentioned even in Apple docs, but not a single handler or any example showing any form of interaction. Every article I found tells me to use a 3rd party package.
Is it possible that SwiftUI charts are just static (why would you even have something like that in 2022)? And if so, how did they create the charts in the Health App?
I figured it out.
It doesn't come out of the box, some coding is needed.
(The algorithm is simple, get the X value for the tap/drag, convert it to chart X and Y, use Point and Rule markers. The example is for (Date, Double) chart values.)
@State var position: ChartPosition?
var body: some View {
Chart {
ForEach(session!.heightData) { height in
AreaMark(
x: .value("Time", height.time.start),
y: .value("Height", height.height.doubleValue(for: diveManager.heightUnit))
)
}
if let position = position {
RuleMark(x: .value("Time", position.x))
.foregroundStyle(.orange.opacity(0.5))
RuleMark(y: .value("Height", position.y))
.foregroundStyle(.orange.opacity(0.5))
PointMark(x: .value("Time", position.x), y: .value("Height", position.y))
.foregroundStyle(.orange)
.symbol(BasicChartSymbolShape.circle.strokeBorder(lineWidth: 3.0))
.symbolSize(250)
}
}
.chartOverlay { proxy in
GeometryReader { geometry in
Rectangle().fill(.clear).contentShape(Rectangle())
.gesture(DragGesture().onChanged { value in updateCursorPosition(at: value.location, geometry: geometry, proxy: proxy) })
.onTapGesture { location in updateCursorPosition(at: location, geometry: geometry, proxy: proxy) }
}
}
}
func updateCursorPosition(at: CGPoint, geometry: GeometryProxy, proxy: ChartProxy) {
let data = session!.heightData
let origin = geometry[proxy.plotAreaFrame].origin
let datePos = proxy.value(atX: at.x - origin.x, as: Date.self)
let firstGreater = data.lastIndex(where: { $0.time.start < datePos! })
if let index = firstGreater {
let time = data[index].time.start
let height = data[index].height.doubleValue(for: diveManager.heightUnit)
position = ChartPosition(x: time, y: height)
}
}