我使用新的 SwiftUI 图表制作了一个简单的条形图,x 轴上是月份名称,y 轴上是成本。我添加了一个
.chartOverlay
视图修改器,用于跟踪我在图表上点击的位置,并显示 BarMark
以及点击的栏成本文本。
这实现起来非常简单,但是当开始点击栏时,我发现点击第一个和第二个栏会产生想要的结果,但是从第三个栏开始,点击栏并交换第二个栏,但是
BarMark
文字显示正确。
我在这个问题上花了几个小时,但找不到这里出了什么问题。
这里显示了完整的代码,如果有人能看到哪里出了问题:
import SwiftUI
import Charts
final class ViewModel: ObservableObject {
let months = ["Nov", "Dec", "Jan", "Feb", "Mar", "Apr"]
let costs = [20.0, 40.0, 80.0, 60.0, 75.0, 30.0]
@Published var forecast: [Forecast] = []
@Published var selectedMonth: String?
@Published var selectedCost: Double?
init() {
forecast = months.indices.map { .init(id: UUID(), month: months[$0], cost: costs[$0]) }
}
}
struct ContentView: View {
@StateObject var viewModel = ViewModel()
var body: some View {
Chart(viewModel.forecast) { data in
BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
.foregroundStyle(Color.blue)
if let selectedMonth = viewModel.selectedMonth, let selectedCost = viewModel.selectedCost {
RuleMark(x: .value("Selected month", selectedMonth))
.annotation(position: .top, alignment: .top) {
VStack {
Text(estimated(for: selectedMonth, and: selectedCost))
}
}
}
}
.chartYAxis {
AxisMarks(position: .leading)
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { location in
updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
}
}
}
}
}
func updateSelectedMonth(at location: CGPoint, proxy: ChartProxy, geometry: GeometryProxy) {
let xPosition = location.x - geometry[proxy.plotAreaFrame].origin.x
guard let month: String = proxy.value(atX: xPosition) else {
return
}
viewModel.selectedMonth = month
viewModel.selectedCost = viewModel.forecast.first(where: { $0.month == month })?.cost
}
func estimated(for month: String, and cost: Double) -> String {
let estimatedString = "estimated: \(cost)"
return estimatedString
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.padding()
}
}
struct Forecast: Identifiable {
let id: UUID
let month: String
let cost: Double
}
尝试这种方法,使用
ForEach
循环并在其后添加 RuleMark
:
var body: some View {
Chart { // <-- here
ForEach(viewModel.forecast) { data in // <-- here
BarMark(x: .value("Month", data.month), y: .value("Kr", data.cost))
.foregroundStyle(Color.blue)
} // <-- here
if let selectedMonth = viewModel.selectedMonth,
let selectedCost = viewModel.selectedCost {
RuleMark(x: .value("Selected month", selectedMonth))
.annotation(position: .top, alignment: .top) {
VStack {
Text(estimated(for: selectedMonth, and: selectedCost))
}
}
}
}
.chartYScale(domain: 0...100) // <-- here
.chartYAxis {
AxisMarks(position: .leading)
}
.chartOverlay { proxy in
GeometryReader { geometry in
ZStack(alignment: .top) {
Rectangle().fill(.clear).contentShape(Rectangle())
.onTapGesture { location in
updateSelectedMonth(at: location, proxy: proxy, geometry: geometry)
}
}
}
}
}
在 iOS 17 中,Apple 引入了一种使用 ChartXSelection 处理图表中选择的本机方法。这使得无需自定义处理即可更轻松地创建交互式可视化。
import SwiftUI
import Charts
struct ChartExample: View {
@State private var barSelection: String?
var body: some View {
Chart {
ForEach(data) { shape in
BarMark(
x: .value("Month", shape.month),
y: .value("Play Count", shape.playCount)
)
.cornerRadius(4)
.foregroundStyle(Color(cgColor: colour3))
}
// Add a rule mark to highlight the selected bar
if let barSelection {
RuleMark(x: .value("Month", barSelection))
.foregroundStyle(.green.opacity(0.5))
.zIndex(-15)
.offset(yStart: -15)
.annotation(position: .top,
spacing: 0,
overflowResolution: .init(x: .disabled, y: .disabled)) {
VStack {
Text(barSelection)
}
}
}
}
.chartXSelection(value: $barSelection)
}
}
// Example data
struct ToyShape: Identifiable {
var id = UUID()
var month: String
var playCount: Int
}
let data = [
ToyShape(month: "January", playCount: 10),
ToyShape(month: "February", playCount: 20),
ToyShape(month: "March", playCount: 15)
]
let colour3 = CGColor(gray: 0.8, alpha: 1)