You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

SwiftUI:如何在弧形文本视图中实现字母等间距?

实现自定义字体的弧形文本等间距解决方案

我正在实现一个弧形文本视图,想要让每个字母之间的间距相等。目前尝试用.font(.system(size: 14, design: .monospaced))虽然能实现等间距,但这不是我需要的目标字体。

当前间距效果:
当前弧形文本间距效果

理想效果(使用自定义字体实现等间距):
理想弧形文本间距效果

现有实现代码

struct ContentView: View {

    @State private var letterWidths: [Int: Double] = [:]
    private let title = "AVENIRNEXT"

    var body: some View {
        ZStack {
            ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
                VStack {
                    Text(String(letter))
                        .font(.custom("AvenirNext-DemiBold", size: 14))
                        .kerning(3)
                        .background(
                            GeometryReader { geometry in // 获取每个字母的宽度
                                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: 300, height: 300 * 0.75)
            .rotationEffect(.degrees(335))
        }
    }

    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()
    }
}

解决方案

现有代码的问题在于根据每个字母的实际宽度计算角度偏移,而自定义字体中不同字母宽度不同,导致最终弧形上的间距不均。可以通过以下两种方式实现等间距:

方式1:均分角度实现等弧间距

直接将弧形的总角度平均分配给每个字母,确保每个字母之间的角度间隔一致,视觉上形成等间距效果:

struct ContentView: View {
    private let title = "AVENIRNEXT"
    // 弧形总覆盖弧度(可根据需求调整,值越大弧形越宽)
    private let totalArcRadians: Double = .pi / 2.5
    private let radius: Double = 125

    var body: some View {
        ZStack {
            ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
                VStack {
                    Text(String(letter))
                        .font(.custom("AvenirNext-DemiBold", size: 14))
                        .kerning(3)
                    Spacer()
                }
                .rotationEffect(fetchAngle(at: index))
            }
            .frame(width: 300, height: 300 * 0.75)
            .rotationEffect(.degrees(335))
        }
    }

    func fetchAngle(at letterPosition: Int) -> Angle {
        let letterCount = Double(title.count)
        // 计算每个字母的角度步长
        let angleStep = totalArcRadians / (letterCount - 1)
        // 起始角度让弧形居中
        let startAngle = -totalArcRadians / 2
        // 当前字母的角度偏移
        let currentAngle = startAngle + angleStep * Double(letterPosition)
        return .radians(currentAngle)
    }
}

方式2:固定字符宽度计算精确弧间距

如果需要更精准控制每个字母之间的弧长距离,可以设定固定的字符占位宽度,再转换为角度偏移:

struct ContentView: View {
    private let title = "AVENIRNEXT"
    private let radius: Double = 125
    // 自定义每个字符的固定占位宽度(根据实际字体调整)
    private let fixedCharWidth: Double = 18
    // 字母间额外字距(对应原代码的kerning值)
    private let extraKerning: Double = 3

    var body: some View {
        ZStack {
            ForEach(Array(title.enumerated()), id: \.offset) { index, letter in
                VStack {
                    Text(String(letter))
                        .font(.custom("AvenirNext-DemiBold", size: 14))
                        .kerning(extraKerning)
                    Spacer()
                }
                .rotationEffect(fetchAngle(at: index))
            }
            .frame(width: 300, height: 300 * 0.75)
            .rotationEffect(.degrees(335))
        }
    }

    func fetchAngle(at letterPosition: Int) -> Angle {
        let letterCount = Double(title.count)
        // 计算总弧长:(固定宽度+额外字距) × 字母间隔数
        let totalArcLength = (fixedCharWidth + extraKerning) * (letterCount - 1)
        // 弧长转弧度:弧度 = 弧长 / 半径
        let totalArcRadians = totalArcLength / radius
        let angleStep = totalArcRadians / (letterCount - 1)
        let startAngle = -totalArcRadians / 2
        let currentAngle = startAngle + angleStep * Double(letterPosition)
        return .radians(currentAngle)
    }
}

方案优势

  • 保留自定义字体的使用,无需切换到等宽字体
  • 无需通过GeometryReader获取字母宽度,代码更简洁高效
  • 两种方式都能确保弧形文本的字母间距完全均匀

内容的提问来源于stack exchange,提问作者Nadia Siddiqah

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 17:34:52