如何在SwiftUI Text中获取每个字符的位置 [英] How to get position of each character in SwiftUI Text

查看:46
本文介绍了如何在SwiftUI Text中获取每个字符的位置的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我的第一个想法是基于 Text + 运算符.似乎很容易,通过组合/一个一个字符/来构建整个文本并检查部分结果的宽度......不幸的是,我没有找到如何做到这一点.所有已知的获取几何图形的技巧(alignmentGuide、GeometryReader、anchorPreferences ...)都可以用作视图修改器!这意味着 Text + 运算符不可用.简单地计算 Text 中字符的位置作为 Text(String(Character)) 宽度的总和不起作用,例如

My first idea was based on Text + operator. Seems to be easy, constructing the whole Text by composition /one by one character/ and check the width of partial result ... Unfortunately, I didn't find the way how to do it. All the tricks known to get some geometry (alignmentGuide, GeometryReader, anchorPreferences ...) works as View modifiers! This means the Text + operator is unusable. Simply calculate the position of characters in Text as a sum of Text(String(Character)) widths doesn't work, for example

Text("WAW")

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

宽度(如预期)不同.

最后我得到了(使用复制粘贴来检查它)类似的东西

Finally I got (use copy paste to check it) something like

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

有了这个结果

哪个有效但非常黑客解决方案

我正在寻找更好的方法!

I am looking for the better way!

更新每个字符的中心

推荐答案

这种方法行不通.字符串的文本布局与单个字符的布局有很大不同.您在此解决的问题是字距调整,但您仍然需要处理连字、组合字符和字母形式(尤其是阿拉伯语).文本在这里是错误的工具.

This approach will not work. Text layout of a string is dramatically different than the layout of individual characters. The thing you're addressing in this is kerning, but you still have ligatures, composing characters, and letter forms (particularly in Arabic) to deal with. Text is the wrong tool here.

你真的不能在 SwiftUI 中做到这一点.您需要使用 CoreText (CTLine) 或 TextKit (NSLayoutManager).

You really can't do this in SwiftUI. You need to use CoreText (CTLine) or TextKit (NSLayoutManager).

也就是说,这并不能保证与 Text 完全匹配.我们不知道 Text 做了哪些事情.例如,当呈现比它想要的更小的框架时,它会收紧间距吗?我们不知道,也不能问(如果有,这种方法也不会处理).但 CoreText 和 TextKit 至少会为您提供可靠的答案,您可以使用它们自行布置与您生成的指标相匹配的文本.

That said, this is not promised to exactly match Text. We don't know what kinds of things Text does. For example, will it tighten spacing when presented with a smaller frame than it desires? We don't know, and we can't ask it (and this approach won't handle it if it does). But CoreText and TextKit will at least give you reliable answers, and you can use them to layout text yourself that matches the metrics you generate.

虽然我认为这种方法不是您想要的方式,但代码本身可以改进.首先,我推荐一个偏好,而不是在 GeometryReader 内部调用 async.

While I don't think this approach is how you want to do it, the code itself can be improved. First, I recommend a preference rather calling async inside of a GeometryReader.

struct WidthKey: PreferenceKey {
    static var defaultValue: [CGFloat] = []
    static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
        value.append(contentsOf: nextValue())
    }
}

您可以使用以下方法捕获宽度数据:

You can capture the width data into that with:

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() 函数来捕获所有宽度,但隐藏结果:

With all that, we can write a captureWidths() function that captures all the widths, but hides the result:

func captureWidths(_ string: String) -> some View {
    Group {
        ForEach(string.runs(), id: \.self) { s in
            Text(verbatim: s).captureWidth()
        }
    }.hidden()
}

请注意字体未设置.这是故意的,它会被这样调用:

Notice that the font isn't set. That's on purpose, it'll be called like this:

        captureWidths(string).font(font)

这将 .font 应用到 Group,后者将其应用到其中的所有文本.

That applies .font to the Group, which applies it to all the Texts inside it.

还要注意这里使用了verbatim(稍后在创建最终文本时).默认情况下,传递给 Text 的字符串不是文字.它们是本地化键.这意味着您需要查找正确的本地化值来分解字符.这增加了一些复杂性,我假设你不想要,所以你应该明确并说这个字符串是逐字的(文字).

Also notice the use of verbatim here (and later when creating the final Text). Strings passed to Text aren't literal by default. They're localization keys. That means you need to look up the correct localized value to break down the characters. That adds some complexity I'm assuming you don't want, so you should be explicit and say this string is verbatim (literal).

一起:

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

看看这个算法如何处理不简单的事情:

To see how this algorithm behaves for things that aren't simple, though:

在从右到左的文本中,这些划分是完全错误的.

In right-to-left text, these divisions are just completely wrong.

注意 T 框太窄了.这是因为在 Zapfino 中,Th 连字比字母 T 加字母 h 宽得多.(公平地说,Text 几乎无法处理 Zapfino;它几乎总是剪辑它.但关键是连字可以显着改变布局,并且存在于许多字体中.)

Note how the T box is much too narrow. That's because in Zapfino, The Th ligature is much wider than the letter T plus the letter h. (In fairness, Text can barely handle Zapfino at all; it almost always clips it. But the point is that ligatures can significantly change layout, and exist in many fonts.)

这篇关于如何在SwiftUI Text中获取每个字符的位置的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆