问题描述
我正在尝试制作一个像这样的简单条形图。 每个条形都应该有一个带有 bar 值的标签。如果是大条形,标签应该在顶部但在里面,如果它很小,则应该在外面。标签的字体应适应用户的喜好。
到目前为止,我已经为一个 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]
}