I'm trying to create a circular text view in SwiftUI where the text starts as a flat line and, as I change the arc degree using a slider, it gradually forms a perfect circle when the degree reaches 360. Here's what I have so far:
import SwiftUI
struct CirculerTextView: View {
@State private var arcDegrees: Double = 0
let text: String = "Happy Mornings"
var body: some View {
VStack(spacing: 40) {
GeometryReader { geometry in
CirculerText(text: text, arcDegrees: arcDegrees, size: geometry.size)
.frame(height: 200)
}
.frame(height: 200)
.border(Color.gray.opacity(0.3))
VStack {
Slider(value: $arcDegrees, in: 0...360, step: 1)
.padding(.horizontal)
Text("Arc Degrees: \(Int(arcDegrees))")
.font(.subheadline)
}
}
.padding()
}
}
struct CirculerText: View {
let text: String
let arcDegrees: Double
let size: CGSize
var body: some View {
let letters = Array(text)
let letterCount = letters.count
let chordLength = size.width * 0.5
let spacing = letterCount > 1 ? chordLength / CGFloat(letterCount - 1) : 0
let startX = (size.width - chordLength) / 2
let centerY = size.height / 2
let centerX = size.width / 2
let totalArcRad = arcDegrees * .pi / 360
let safeArcRad = max(abs(totalArcRad), 0.0001)
let radius = chordLength / (2 * CGFloat(sin(safeArcRad / 2)))
let circleCenter = CGPoint(x: centerX, y: centerY)
return ZStack {
ForEach(0..<letterCount, id: \.self) { index in
let letterAngle = -totalArcRad / 2 + totalArcRad * Double(index) / Double(letterCount - 1)
let x = circleCenter.x + radius * CGFloat(sin(letterAngle))
let y = circleCenter.y - radius * CGFloat(cos(letterAngle))
let finalPos = CGPoint(x: x, y: y)
let textRotation = arcDegrees == 0 ? 0 : -angleInDegrees(letterAngle)
Text(String(letters[index]))
.font(.title)
.position(finalPos)
.rotationEffect(.degrees(textRotation))
}
}
}
func angleInDegrees(_ radians: Double) -> Double {
return radians * 360 / .pi
}
}
struct CirculerTextView_Previews: PreviewProvider {
static var previews: some View {
CirculerTextView()
}
}
The idea is that as I move the slider, the text should gradually transition from a flat line to a full circle as the arcDegrees value increases from 0 to 360. Right now, it works for creating a circular arrangement of text, but I'm struggling to get it to transition smoothly, especially when the degrees are smaller (less than 360), where the text still appears straight.
How can I modify the code so that:
The text smoothly transitions from a flat line to a circular arc. When the degree reaches 360, the text perfectly forms a circle. Any help would be greatly appreciated!
what I am trying to achieve
what I am getting result right now
A SwiftUI solution for curved text can be found in the answer to SwiftUI: How to have equal spacing between letters in a curved text view? (it was my answer).
That solution takes the circle radius as parameter, from which it computes the angle. Here is an adapted version that uses your naming and takes the angle as parameter, from which it computes the radius. An angle of 0 requires special handling, because the radius would be infinity.
// Adaption of CurvedText, see
// https://stackoverflow.com/a/77280669/20386264
// for code comments and more explanation
struct CircularText: View {
let text: String
let arcAngle: Angle
var body: some View {
if arcAngle.radians == 0.0 {
textAsChars
} else {
textAsChars
.fixedSize()
.hidden()
.overlay {
GeometryReader { fullText in
let textWidth = fullText.size.width
let startAngle = -(arcAngle.radians / 2)
let radius = textWidth / arcAngle.radians
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.offset) { index, character in
Text(String(character))
.hidden()
.overlay {
GeometryReader { charSpace in
let midX = charSpace.frame(in: .named("FullText")).midX
let fraction = midX / textWidth
let angle = startAngle + (fraction * arcAngle.radians)
let xOffset = (textWidth / 2) - midX
Text(String(character))
.offset(y: -radius)
.rotationEffect(.radians(angle))
.offset(x: xOffset, y: radius)
}
}
}
}
.fixedSize()
.frame(width: textWidth)
}
}
.coordinateSpace(name: "FullText")
}
}
private var textAsChars: some View {
HStack(spacing: 0) {
ForEach(Array(text.enumerated()), id: \.offset) { index, character in
Text(String(character))
}
}
}
}
Example use:
struct CircularTextView: View {
@State private var arcDegrees: Double = 0
let text: String = "The quick brown fox jumps over the lazy dog"
var body: some View {
VStack {
CircularText(text: text, arcAngle: .degrees(arcDegrees))
.frame(height: 300)
.border(Color.gray.opacity(0.3))
Slider(value: $arcDegrees, in: -360...360, step: 1)
.padding(.horizontal)
Text("Arc Degrees: \(Int(arcDegrees))")
.font(.subheadline)
}
.padding()
}
}
Note that a GeometryReader
is not needed, because the text adopts its natural width.