I'm trying to do a simple Bar Chart like this one. Every bar should have a label with the value of bar. If it's a big bar the label should be at the top but on the inside, if it's small it should be just outside. The font of the label should adapt to the user's preference.
So far I have come up with this code for one bar, the 7 bars in the picture are just an HStack() with seven different data points.
struct SingleBar: View {
let theCategory: String
let theValue: Double
let theMax: Double
var body: some View {
VStack {
GeometryReader { geometry in
let theHeight = (CGFloat(theValue / theMax) * geometry.size.height)
let theSpace = geometry.size.height - theHeight
let labelPositionY = (theSpace > (geometry.size.height / 2)) ? theSpace - 30 : theSpace + 10
RoundedRectangle(cornerRadius: 5.0)
.fill(LinearGradient(gradient: Gradient(colors: gradientColors), startPoint: .bottom, endPoint: .top))
.frame(height: theHeight)
.padding(.horizontal, 10)
.offset(x: 0, y:theSpace)
Text(theValue.fixedLength1)
.frame(width: geometry.size.width, alignment: .center)
.rotationEffect(Angle.degrees(270), anchor: .center)
.offset(x: 0, y: labelPositionY)
}
Text(theCategory)
.lineLimit(1)
}
}
}
My question is: how do I figure out the length (in pixels / points) of the label so that I can move it to the correct position with var labelPositionY? For now I have iterated for one setting of font size, but I want it to be perfect in all conditions...
PS: theValue.fixedLength1 is a computed property that returns a formatted string
My solution
So... after realizing the question has already been answered in a different post I wanted to share my "final" solution:
struct ViewSizeKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
value = nextValue()
}
}
struct ViewGeometry: View {
var body: some View {
GeometryReader { geometry in
Color.clear
.preference(key: ViewSizeKey.self, value: geometry.size)
}
}
}
struct SingleBar: View {
@State var labelDimensions = CGSize(width: 0, height: 0)
let theCategory: String
let theValue: Double
let theMax: Double
let theTarget: Double
var body: some View {
VStack {
GeometryReader { geometry in
let theHeight = (CGFloat(theValue / theMax) * geometry.size.height)
let theSpace = geometry.size.height - theHeight
let labelPositionY = (theHeight > geometry.size.height / 4) ?
theSpace + labelDimensions.width / 2 :
theSpace - labelDimensions.width / 2 - labelDimensions.height
let labelPositionX = (geometry.size.width - labelDimensions.width) / 2
RoundedRectangle(cornerRadius: 5.0)
.frame(height: theHeight)
.padding(.horizontal, 10)
.offset(x: 0, y:theSpace)
Text(theValue.fixedLength1)
.background(ViewGeometry())
.onPreferenceChange(ViewSizeKey.self) { labelDimensions = $0 }
.rotationEffect(Angle.degrees(270), anchor: .center)
.offset(x: labelPositionX, y: labelPositionY)
}
Text(theCategory)
.lineLimit(1)
}
}
}