0

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.

Illustration: iPhone Sample Bar Chart

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

Gin Tonyx
  • 383
  • 1
  • 11

1 Answers1

0

Usage,

struct ContentView: View {
    @State var textSize: CGRect = .zero
    var body: some View {
        VStack {
            Rectangle()
                .frame(width: textSize.size.width, height: textSize.size.height)
            Text("Hello, world!")
                .padding()
                .saveBounds(viewId: 1)
        }
        .retrieveBounds(viewId: 1, $textSize)
    }
}

add these extension and structs to your project.

extension View {
    public func saveBounds(viewId: Int, coordinateSpace: CoordinateSpace = .global) -> some View {
        background(GeometryReader { proxy in
            Color.clear.preference(key: SaveBoundsPrefKey.self, value: [SaveBoundsPrefData(viewId: viewId, bounds: proxy.frame(in: coordinateSpace))])
        })
    }
    
    public func retrieveBounds(viewId: Int, _ rect: Binding<CGRect>) -> some View {
        onPreferenceChange(SaveBoundsPrefKey.self) { preferences in
            DispatchQueue.main.async {
                // The async is used to prevent a possible blocking loop,
                // due to the child and the ancestor modifying each other.
                let p = preferences.first(where: { $0.viewId == viewId })
                rect.wrappedValue = p?.bounds ?? .zero
            }
        }
    }
}

struct SaveBoundsPrefData: Equatable {
    let viewId: Int
    let bounds: CGRect
}

struct SaveBoundsPrefKey: PreferenceKey {
    static var defaultValue: [SaveBoundsPrefData] = []
    
    static func reduce(value: inout [SaveBoundsPrefData], nextValue: () -> [SaveBoundsPrefData]) {
        value.append(contentsOf: nextValue())
    }
    
    typealias Value = [SaveBoundsPrefData]
}
YodagamaHeshan
  • 4,996
  • 2
  • 26
  • 36
  • Thank you for your answer. I can't immediately grasp if it will solve my problem. I first tried the solution offered in the comment linking my question to [the other question with answer](https://stackoverflow.com/questions/64452647/how-we-can-get-and-read-size-of-a-text-with-geometryreader-in-swiftui). And I got it to work so I'm fine for now. -- BUT: I can see from your solution that it would work with multiple boxes I need the size of. If I ever run into that problem I'll try yours... – Gin Tonyx Dec 31 '20 at 09:43