SwiftUI沿圆弧路径添加文本 解决布局中心重叠问题
SwiftUI环形图分段文本对齐方案
问题说明
需要在自定义环形图的各圆弧段内部添加对应标注文本,测试阶段尝试为earthyStatsInTonnes对应的角度位置添加角度文本,使用GeometryReader实现布局时,所有文本都堆叠在视图中心互相重叠,无法对齐到对应圆弧段。
原有实现代码:
struct Shapes: View { private enum Params { static var viewSizeOffset: CGFloat = 100 static let lineWidth: CGFloat = 20 static var halfLineWidth: CGFloat { lineWidth / 2 } static let animationDuration: CGFloat = 0.05 // nominal animation to smooth things static let circleDegrees: CGFloat = 360 static let circleRotation: Angle = .init(degrees: -90) static let offsetFactor: CGFloat = 0.4 static let minCircleAngle: CGFloat = 00 static let maxCircleAngle: CGFloat = 360 static var minCircleTrim: CGFloat { minCircleAngle / circleDegrees } static var maxCircleTrim: CGFloat { maxCircleAngle / circleDegrees } static let progressColor = Color.red static let backgroundColor = Color.blue static let arcFrame: CGFloat = 225 } @State private var rotation: Double = 0 @State var earthyStatsInTonnes = [2.6, 3.0, 5.4, 6.75, 0.0] @State private var increment = 0 let arcColor = [Color.red, Color.gray, Color.black, Color.green, Color.purple, Color.blue] var body: some View { GeometryReader { geo in VStack { ZStack { Text("Text \(angle)") .position( x: 0, y: 0 ) Circle() .fill(.red.opacity(0.2)) .frame(width: Params.arcFrame + 36, height: Params.arcFrame + 36) ForEach(earthyStatsInTonnes.indices) { stat in VStack { ZStack { arcView(startAngle: stat == 0 ? 00 : earthyStatsInTonnes[stat], endAngle: earthyStatsInTonnes[stat], total: 17.8, arcColor: arcColor[stat]) } .zIndex(-Double(stat)) } } } .frame(width: Params.arcFrame, height: Params.arcFrame) .padding(45) } .position(x: geo.size.width / 2, y: geo.size.height / 2) } } private func arcView(startAngle: Double, endAngle: Double, total: Double, arcColor: Color) -> some View { return Arc(startAngle: .degrees(startAngle * total), endAngle: .degrees(startAngle * total + (endAngle * total)), clockwise: true) .stroke(arcColor, style: .init(lineWidth: 36, lineCap: .round, lineJoin: .miter, miterLimit: 0, dash: [2, 3], dashPhase: 1)) .frame(width: 226, height: 229) } func onDrag(value: DragGesture.Value) { let circleDegrees = Params.circleDegrees // Calculate radians let vector = CGVector(dx: value.location.x, dy: value.location.y) let radians = atan2( vector.dy - Params.halfLineWidth, vector.dx - Params.halfLineWidth ) var angle = radians * circleDegrees / 2 / .pi // atan2 gives -180 to 180, so set range to 0 to 360 if angle < 0 { angle += circleDegrees } // constrain angle between desired range angle = max(angle, Params.minCircleAngle) angle = min(angle, Params.maxCircleAngle) withAnimation(Animation.linear(duration: Params.animationDuration)) { self.angle = Double(angle) } } } struct Arc: Shape { var startAngle: Angle var endAngle: Angle var clockwise: Bool func path(in rect: CGRect) -> Path { let rotationAdjustment = Angle.degrees(90) let modifiedStart = startAngle - rotationAdjustment let modifiedEnd = endAngle - rotationAdjustment var path = Path() path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: rect.width / 2, startAngle: modifiedStart, endAngle: modifiedEnd, clockwise: !clockwise) return path } }
修复步骤
文本重叠的核心原因是没有根据每段圆弧的实际位置计算文本坐标,直接固定位置放置自然会全部堆叠在中心,原有代码还存在angle状态变量未声明、圆弧渲染尺寸不统一的问题,按以下逻辑修改即可:
- 统一圆弧尺寸,把
arcView里的frame从226/229调整为和Params中定义的arcFrame一致,避免坐标计算和实际渲染尺寸偏差 - 遍历分段数据时累计每段的起始角度,计算每段圆弧的中点角度(段起始角度 + 段自身角度/2),文本需要放在这个中点角度对应的半径位置
- 角度转坐标时和Arc形状的角度偏移对齐:Arc默认把0度偏移到了顶部,计算三角函数时要做-90度的角度转换,同时适配SwiftUI y轴向下的坐标系规则
- 文本放置半径设为圆弧半径减去线宽的一半,刚好落在圆弧段内部中心位置
核心坐标计算逻辑:
// 环形基础参数 let totalValue = earthyStatsInTonnes.reduce(0, +) let arcCenter: CGFloat = Params.arcFrame / 2 // 36为圆弧线宽,减去一半让文本落在圆弧内部中心 let textLayoutRadius = arcCenter - 18 var accumulatedAngle: Double = 0 // ZStack内和圆弧同级渲染文本 ForEach(Array(earthyStatsInTonnes.enumerated()), id: \.offset) { index, value in let segmentAngle = value / totalValue * 360 let midAngle = accumulatedAngle + segmentAngle / 2 accumulatedAngle += segmentAngle // 角度转iOS坐标系弧度 let radian = (midAngle - 90) * .pi / 180 let textPosX = arcCenter + textLayoutRadius * cos(radian) let textPosY = arcCenter + textLayoutRadius * sin(radian) Text("\(value)") .position(x: textPosX, y: textPosY) }
把上述文本渲染逻辑加入ZStack后,文本会自动对齐到对应圆弧段的中心位置,不会出现重叠问题。
内容的提问来源于stack exchange,提问作者Robert
相关产品推荐
相关产品推荐

