最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

ios - How to create circular text in SwiftUI that goes from a flat line to a perfect circle based on degree changes? - Stack Ove

programmeradmin0浏览0评论

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

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

Share Improve this question asked Feb 3 at 6:21 HeWhoRemainsHeWhoRemains 6111 bronze badges 0
Add a comment  | 

1 Answer 1

Reset to default 2

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/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.

与本文相关的文章

发布评论

评论列表(0)

  1. 暂无评论