SwiftUI:如何获取 GeometryReader 中文本的长度以磅为单位

问题描述

我正在尝试制作一个像这样的简单条形图。 每个条形都应该有一个带有 bar 值的标签。如果是大条形,标签应该在顶部但在里面,如果它很小,则应该在外面。标签的字体应适应用户的喜好。

Illustration: iPhone Sample Bar Chart

到目前为止,我已经为一个 bar 想出了这个代码,图中的 7 个 bar 只是一个带有 7 个不同数据点的 HStack()。

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

我的问题是:如何计算标签的长度(以像素/点为单位),以便我可以使用 var labelPositionY 将其移动到正确的位置?现在我已经迭代了一种字体大小设置,但我希望它在所有条件下都是完美的...

PS: theValue.fixedLength1 是一个计算属性,它返回一个格式化的字符串


我的解决方

所以......在意识到问题已经在另一篇文章中得到回答后,我想分享我的“最终”解决方案:

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

解决方法

用法,

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

将这些扩展和结构添加到您的项目中。

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