I want to center textView in ArcShape, currently textView's startAngle is set to ArcShape's startAngle. So far, I've tried finding the midpoint of the ArcShape's startAngle and endAngle, but this shifts the textView to the right of the ArcShape.
How do I calculate the startAngle of the textView so that it centered in the ArcShape?
This is what it looks like currently:
Ideally, I want it to look like this:
import SwiftUI
struct ContentView: View {
@State private var letterWidths: [Int: Double] = [:]
let size: CGFloat = 300
var body: some View {
ZStack {
ArcShape(startAngle: Angle(degrees: 180), endAngle: Angle(degrees: 230))
.foregroundColor(.green)
textView("HELLO", startAngle: Angle(degrees: 180))
ArcShape(startAngle: Angle(degrees: 233), endAngle: Angle(degrees: 300))
.foregroundColor(.red)
textView("BYEBYE", startAngle: Angle(degrees: 233))
}
.frame(width: size, height: size)
.frame(height: size / 2)
.offset(y: size / 4.5)
}
@ViewBuilder
func textView(_ title: String, startAngle: Angle) -> some View {
ZStack {
ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
VStack {
Text(String(letter))
.font(.system(size: 14, weight: .semibold, design: .monospaced))
.kerning(2)
.background(
GeometryReader { geometry in // using this to get the width of EACH letter
Color.clear
.preference(
key: LetterWidthPreferenceKey.self,
value: geometry.size.width
)
}
)
.onPreferenceChange(LetterWidthPreferenceKey.self, perform: { width in
letterWidths[index] = width
})
Spacer()
}
.rotationEffect(fetchAngle(at: index))
}
.frame(width: size, height: size * 0.75)
.rotationEffect(startAngle + .degrees(90))
}
}
func fetchAngle(at letterPosition: Int) -> Angle {
let timesPi: (Double) -> Double = { $0 * .pi }
let radius: Double = 125
let circumference = timesPi(radius)
let finalAngle = timesPi(
letterWidths
.filter { $0.key <= letterPosition }
.map(\.value)
.reduce(0, +) / circumference
)
return .radians(finalAngle)
}
}
struct LetterWidthPreferenceKey: PreferenceKey {
static var defaultValue: Double = 0
static func reduce(value: inout Double, nextValue: () -> Double) {
value = nextValue()
}
}
#Preview {
ContentView()
}
private struct ArcShape: Shape {
let startAngle: Angle?
let endAngle: Angle?
func path(in rect: CGRect) -> Path {
let shorterLength = min(rect.width, rect.height)
let path = UIBezierPath(
roundedArcCenter: rect.center,
innerRadius: (shorterLength / 2) * 0.54,
outerRadius: (shorterLength / 2) * 0.90,
startAngle: startAngle ?? .zero,
endAngle: endAngle ?? .zero,
cornerRadiusPercentage: 0.01
)
return Path(path.cgPath)
}
}
extension UIBezierPath {
public convenience init(roundedArcCenter center: CGPoint, innerRadius: CGFloat, outerRadius: CGFloat, startAngle: Angle, endAngle: Angle, cornerRadiusPercentage: CGFloat) {
let maxCornerRadiusBasedOnInnerArcLength = abs((endAngle - startAngle).radians) * innerRadius / 2
let maxCornerRadiusBasedOnOuterArcLength = abs((endAngle - startAngle).radians) * outerRadius / 2
let maxCornerRadiusBasedOnEndCapLength = (outerRadius - innerRadius) / 2
let outerCornerRadius = min(2 * .pi * outerRadius * cornerRadiusPercentage, maxCornerRadiusBasedOnOuterArcLength, maxCornerRadiusBasedOnEndCapLength)
let outerCornerRadiusPercentage = outerCornerRadius / (2 * .pi * outerRadius)
let innerCornerRadius = min(2 * .pi * innerRadius * outerCornerRadiusPercentage, maxCornerRadiusBasedOnInnerArcLength, maxCornerRadiusBasedOnEndCapLength)
let innerInsetAngle = Angle(radians: innerCornerRadius / innerRadius)
let outerInsetAngle = Angle(radians: outerCornerRadius / outerRadius)
self.init()
var arcStartAngle = (startAngle + outerInsetAngle).radians
var arcEndAngle = (endAngle - outerInsetAngle).radians
addArc(
withCenter: .zero,
radius: outerRadius,
startAngle: min(arcStartAngle, arcEndAngle),
endAngle: max(arcStartAngle, arcEndAngle),
clockwise: true
)
addCorner(
to: .pointOnCircle(radius: outerRadius - outerCornerRadius, angle: endAngle),
controlPoint: .pointOnCircle(radius: outerRadius, angle: endAngle)
)
addLine(to: .pointOnCircle(radius: innerRadius + innerCornerRadius, angle: endAngle))
addCorner(
to: .pointOnCircle(radius: innerRadius, angle: endAngle - innerInsetAngle),
controlPoint: .pointOnCircle(radius: innerRadius, angle: endAngle)
)
arcStartAngle = (endAngle - innerInsetAngle).radians
arcEndAngle = (startAngle + innerInsetAngle).radians
addArc(
withCenter: .zero,
radius: innerRadius,
startAngle: max(arcStartAngle, arcEndAngle),
endAngle: min(arcStartAngle, arcEndAngle),
clockwise: false
)
addCorner(
to: .pointOnCircle(radius: innerRadius + innerCornerRadius, angle: startAngle),
controlPoint: .pointOnCircle(radius: innerRadius, angle: startAngle)
)
addLine(to: .pointOnCircle(radius: outerRadius - outerCornerRadius, angle: startAngle))
addCorner(
to: .pointOnCircle(radius: outerRadius, angle: startAngle + outerInsetAngle),
controlPoint: .pointOnCircle(radius: outerRadius, angle: startAngle)
)
apply(.init(translationX: center.x, y: center.y))
}
private func addCorner(to: CGPoint, controlPoint: CGPoint) {
let circleApproximationConstant = 0.551915
addCurve(
to: to,
controlPoint1: currentPoint + (controlPoint - currentPoint) * circleApproximationConstant,
controlPoint2: to + (controlPoint - to) * circleApproximationConstant
)
}
}
private extension CGPoint {
static func pointOnCircle(radius: CGFloat, angle: Angle) -> CGPoint {
CGPoint(x: radius * Darwin.cos(angle.radians), y: radius * Darwin.sin(angle.radians))
}
static func + (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
static func - (lhs: CGPoint, rhs: CGPoint) -> CGPoint {
CGPoint(x: lhs.x - rhs.x, y: lhs.y - rhs.y)
}
static func * (lhs: CGPoint, rhs: CGFloat) -> CGPoint {
CGPoint(x: lhs.x * rhs, y: lhs.y * rhs)
}
}
public extension CGRect {
var center: CGPoint {
CGPoint(x: size.width / 2.0, y: size.height / 2.0)
}
}


With your current code, you can do it in a similar way to how you'd centre something horizontally/vertically.
You calculate the available space, and how much space the text takes up. Subtract the latter from the former and divide by 2. Then you "shift" the text by that amount. Of course, in this case the "space" we are talking about is measured in degrees/radians.
This means
textViewshould also take in the end angle:The
rotationEffectapplied to theForEachshould have theoffsetadded:Finally, note that your
fetchAngleis incorrect. The "H" in "Hello" should be completely horizontal (withoutoffset), but you can see that is not the case in your screenshot. The comparison in thefiltershould be<, not<=.Output: