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
相关产品推荐
相关产品推荐

