First of all, I am quite new to SwiftUI. I have a problem with the annotation of the BarMarks. The annotation is covered by the following BarMarks. Is there a way to prevent this?
import Foundation
import SwiftUI
import Charts
struct WaterConsumptionChart: View {
@ObservedObject var model: ConsumptionDataModel
@State private var selectedItem: ConsumptionData?
@State private var showTooltip: Bool = false
private var gradientColors = [Color.primarySwiftUI.opacity(0.8),
Color.groheLink.opacity(0.8)]
var body: some View {
VStack {
Chart(model.data) { item in
BarMark(
x: .value("", item.text),
y: .value("consumption", item.consumption ?? 0),
width: .automatic
)
.foregroundStyle(LinearGradient(
gradient: Gradient(colors: gradientColors),
startPoint: .top,
endPoint: .bottom
))
.cornerRadius(4)
.annotation(position: .top, alignment: .center) {
let show = ((selectedItem?.text ?? "") == item.text) && ((item.consumption ?? 0) > 0)
TooltipView(value: "\(selectedItem?.text ?? "") • \(selectedItem?.consumption ?? 0)l")
.opacity(show ? 1 : 0)
.offset(y: -2)
}
if let average = model.average, average > 0 {
RuleMark(y: .value("", average))
.foregroundStyle(.primarySwiftUI)
.lineStyle(.init(lineWidth: 1))
} else {
RuleMark(y: .value("", 0.0))
.foregroundStyle(.primarySwiftUI)
.lineStyle(.init(lineWidth: 0))
}
}
.padding([.leading, .trailing], 16)
.chartXAxis {
AxisMarks(values: .automatic(minimumStride: 3, desiredCount: 7)) { value in
let period = viewModel.period?.period
let modulo = period == .day ? 4 : 5
let entry = Int(value.as(String.self) ?? "") ?? 0
if value.count <= 12 || entry % modulo == 0 || value.index == 0 {
let uiFont = Fonts.Inter.regular.of(size: 10)
AxisValueLabel()
.foregroundStyle(.primarySwiftUI)
.font(Font(uiFont))
.offset(x: 0, y: 10)
AxisGridLine()
.foregroundStyle(.greyE1E3E7)
.offset(x: 0, y: 0)
}
}
}
.chartYAxis {
AxisMarks(values: .automatic(desiredCount: 7)) { value in
if let intValue = value.as(Int.self) {
if intValue == 0 {
AxisGridLine()
.foregroundStyle(.primarySwiftUI)
} else {
let uiFont = Fonts.Inter.regular.of(size: 10)
AxisValueLabel("\(intValue)l")
.foregroundStyle(.primarySwiftUI)
.font(Font(uiFont))
AxisGridLine()
.foregroundStyle(.greyE1E3E7)
}
}
}
}
.animation(.smooth, value: viewModel.data)
.chartOverlay { pr in
GeometryReader { geoProxy in
Rectangle()
.fill(.clear).contentShape(Rectangle()).padding([.leading, .trailing], 16)
.onTapGesture(perform: { value in
let origin = geoProxy[pr.plotAreaFrame].origin
let location = CGPoint(x: value.x - origin.x, y: value.y - origin.y)
if let selected = pr.value(atX: location.x, as: String.self),
let dataItem = viewModel.data.first(where: { $0.text == selected }) {
if dataItem.consumption ?? 0 > 0 { UIImpactFeedbackGenerator(style: .light).impactOccurred() }
self.selectedItem = dataItem
} else {
self.selectedItem = nil
}
})
}
}
}
}
}
I tried to experiment with .zIndex(value: Double) but unfortunately I could not solve the problem. I also need compatibility with iOS 16 and .zIndex is only available from iOS 17. Does anyone have an idea?
The best way to ensure annotations are above all bars is to draw them separately. Instead of adding .annotation() inside the BarMark, create a second Chart layer on top (cleaner even if zIndex would be available).
Chart {
// First layer: Bars (rendered first, in background)
ForEach(model.data, id: \.text) { item in
BarMark(
x: .value("", item.text),
y: .value("consumption", item.consumption ?? 0),
width: .automatic
)
.foregroundStyle(LinearGradient(
gradient: Gradient(colors: gradientColors),
startPoint: .top,
endPoint: .bottom
))
.cornerRadius(4)
}
// Second layer: Annotations (rendered last, on top)
ForEach(model.data, id: \.text) { item in
if let selectedItem = selectedItem, selectedItem.text == item.text, let consumption = selectedItem.consumption, consumption > 0 {
PointMark(
x: .value("", item.text),
y: .value("consumption", item.consumption ?? 0)
)
.annotation(position: .top, alignment: .center) {
TooltipView(value: "\(selectedItem.text) • \(consumption)l")
.opacity(1)
.offset(y: -2)
}
}
}
// Third layer: RuleMark (always on top of bars)
if let average = model.average, average > 0 {
RuleMark(y: .value("", average))
.foregroundStyle(.primarySwiftUI)
.lineStyle(.init(lineWidth: 1))
}
}