Как получить положение каждого символа в тексте SwiftUI - PullRequest
1 голос
/ 21 января 2020

Моя первая идея была основана на операторе Text +. Кажется, что это легко, построить весь текст по композиции / один за другим / и проверить ширину частичного результата ... К сожалению, я не нашел способ, как это сделать. Все приемы, известные для получения некоторой геометрии (alignmentGuide, GeometryReader, anchorPreferences ...), работают как модификаторы View! Это означает, что оператор Text + не может использоваться. Просто рассчитать положение символов в тексте как сумму ширины текста (строки (символа)) не работает, например

Text("WAW")

и

HStack(spacing:0) { Text("W"); Text("A"); Text("W") }

ширина (как ожидается) другой.

Наконец-то я получил (используйте копировать вставить, чтобы проверить это) что-то вроде

struct ContentView: View {
    @State var width: [CGFloat] = []
    let font = Font.system(size: 100)
    var body: some View {
        VStack {
            if width.isEmpty {
                text(t: Text("W").font(font), width: $width)
                text(t: Text("WA").font(font), width: $width)
                text(t: Text("WAW").font(font), width: $width)
                text(t: Text("WAWE").font(font), width: $width)
            } else {
                ZStack(alignment: .topLeading) {
                    Text("WAWE").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(Array(width.sorted(by: <).enumerated()), id: \.0) { p in
                        ZStack {
                            Path { path in
                                path.move(to: CGPoint(x: p.1, y: 0))
                                path.addLine(to: CGPoint(x: p.1, y: 150))
                            }.stroke(lineWidth: 1)

                            Text("\(p.1)").rotationEffect(Angle(degrees: 90), anchor: .bottom).position(CGPoint(x: p.1, y: 170))
                        }
                    }
                }.padding()
            }
        }
    }
}
func text(t: Text, width: Binding<[CGFloat]>)->some View {
    let tt = t.background(
        GeometryReader{ proxy->Color in
            DispatchQueue.main.async {
                width.wrappedValue.append(proxy.size.width)
            }
            return Color.clear
        }
    )
    return tt.background(Color.yellow)
}

с таким результатом

enter image description here

Который работает, но является очень хакерским решением

Я ищу лучший путь!

ОБНОВЛЕНИЕ с центром каждого символа enter image description here

1 Ответ

1 голос
/ 21 января 2020

Этот подход не будет работать. Разметка текста строки кардинально отличается от разметки отдельных символов. В этом вопросе вы обращаетесь к кернингу, но у вас все еще есть лигатуры, сочинения символов и буквенные формы (особенно на арабском 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 }
    }
}

Чтобы увидеть как этот алгоритм ведет себя для непростых вещей:

enter image description here

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

enter image description here

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...