Этот подход не будет работать. Разметка текста строки кардинально отличается от разметки отдельных символов. В этом вопросе вы обращаетесь к кернингу, но у вас все еще есть лигатуры, сочинения символов и буквенные формы (особенно на арабском c), с которыми нужно иметь дело. Текст здесь не тот инструмент.
Вы действительно не можете сделать это в SwiftUI. Вам нужно использовать CoreText (CTLine) или TextKit (NSLayoutManager).
Тем не менее, это не обещает точно соответствовать тексту. Мы не знаем, что делает Text. Например, будет ли он уменьшать расстояние, когда он представлен с меньшей рамой, чем он желает? Мы не знаем, и мы не можем спросить об этом (и этот подход не справится с этим, если он это сделает). Но CoreText и TextKit, по крайней мере, дадут вам надежные ответы, и вы можете использовать их для самостоятельной компоновки текста, который соответствует генерируемым метрикам.
Хотя я не думаю, что такой подход - это то, что вы хотите сделать это, сам код может быть улучшен. Во-первых, я рекомендую предпочтение вместо вызова asyn c внутри GeometryReader.
struct WidthKey: PreferenceKey {
static var defaultValue: [CGFloat] = []
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
Вы можете записать данные ширины в это с помощью:
extension View {
func captureWidth() -> some View {
background(GeometryReader{ g in
Color.clear.preference(key: WidthKey.self, value: [g.size.width])
})
}
}
Это будет прочитано позже с onPreferenceChange
:
.onPreferenceChange(WidthKey.self) { self.widths = $0 }
И в качестве помощника для строки:
extension String {
func runs() -> [String] {
indices.map { String(prefix(through: $0)) }
}
}
При всем этом мы можем написать функцию captureWidths (), которая захватывает все ширины, но скрывает результат:
func captureWidths(_ string: String) -> some View {
Group {
ForEach(string.runs(), id: \.self) { s in
Text(verbatim: s).captureWidth()
}
}.hidden()
}
Обратите внимание, что шрифт не установлен. Это специально, это будет называться так:
captureWidths(string).font(font)
Это относится .font
к группе, которая применяется ко всем текстам внутри нее.
Также обратите внимание на использование verbatim
здесь (и позже при создании окончательного текста). Строки, переданные в Text, не являются литеральными по умолчанию. Это ключи локализации. Это означает, что вам нужно найти правильное локализованное значение, чтобы разбить символы. Это добавляет сложности, я полагаю, вы не хотите, так что вы должны быть явными и сказать, что эта строка дословная (буквальная).
И все вместе:
struct WidthKey: PreferenceKey {
static var defaultValue: [CGFloat] = []
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
}
extension View {
func captureWidth() -> some View {
background(GeometryReader{ g in
Color.clear.preference(key: WidthKey.self, value: [g.size.width])
})
}
}
extension String {
func runs() -> [String] {
indices.map { String(prefix(through: $0)) }
}
}
func captureWidths(_ string: String) -> some View {
Group {
ForEach(string.runs(), id: \.self) { s in
Text(s).captureWidth()
}
}.hidden()
}
struct ContentView: View {
@State var widths: [CGFloat] = []
@State var string: String = "WAWE"
let font = Font.system(size: 100)
var body: some View {
ZStack(alignment: .topLeading) {
captureWidths(string).font(font)
Text(verbatim: string).font(font).border(Color.red)
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: 150))
}.stroke(lineWidth: 1)
Text("\(0)").rotationEffect(Angle(degrees: 90), anchor: .bottom)
.position(CGPoint(x: 0, y: 170))
ForEach(widths, id: \.self) { p in
ZStack {
Path { path in
path.move(to: CGPoint(x: p, y: 0))
path.addLine(to: CGPoint(x: p, y: 150))
}.stroke(lineWidth: 1)
Text("\(p)").rotationEffect(Angle(degrees: 90), anchor: .bottom).position(CGPoint(x: p, y: 170))
}
}
}
.padding()
.onPreferenceChange(WidthKey.self) { self.widths = $0 }
}
}
Чтобы увидеть как этот алгоритм ведет себя для непростых вещей:

В тексте справа налево эти деления просто совершенно неверны.

Обратите внимание, что поле T слишком узкое. Это потому, что в Zapfino, лигатура Th гораздо шире, чем буква T плюс буква h. (Справедливости ради, Text практически не справляется с Zapfino; он почти всегда обрезает его. Но дело в том, что лигатуры могут существенно изменить макет и существуют во многих шрифтах.)