SwiftUI:是否存在用于突出显示Text()视图的子串的修饰符?

人气:772 发布:2022-10-16 标签: swiftui swift

问题描述

我的屏幕上有一些文本:

Text("someText1")

是否可以在不创建批次文本项目的情况下突出显示/选择部分文本

我是说

Text("som") + Text("eTex").foregroundColor(.red) + Text("t1")

不是我的解决方案

最好有某种修饰符,以某种方式突出显示文本的一部分。类似于:

Text("someText1").modifier(.highlight(text:"eTex"))

有可能吗?(我的意思是不创建很多视图)

推荐答案

一旦创建文本,就无法将其重新打开。您的示例造成了本地化问题。someText1实际上不是要打印的字符串。它是字符串的本地化关键字。缺省的本地化字符串恰好是键,所以它是有效的。当您本地化时,搜索eTex的尝试将悄悄中断。因此,这不是一个好的通用接口。

即便如此,构建解决方案也非常有启发性,可能对特定案例很有用。

基本目标是将样式视为应用于范围的属性。这正是NSAttributedString为我们提供的功能,包括合并和拆分范围以管理多个重叠属性的能力。NSAttributedString对SWIFT不是特别友好,因此从零开始重新实现它可能会有一些价值,但我只是打算将其隐藏为实现细节。

因此,TextStyle将是一个NSAttributedString.Key和一个将一个文本转换为另一个文本的函数。

public struct TextStyle {
    // This type is opaque because it exposes NSAttributedString details and
    // requires unique keys. It can be extended by public static methods.

    // Properties are internal to be accessed by StyledText
    internal let key: NSAttributedString.Key
    internal let apply: (Text) -> Text

    private init(key: NSAttributedString.Key, apply: @escaping (Text) -> Text) {
        self.key = key
        self.apply = apply
    }
}

TextStyle不透明。为了构造它,我们公开了一些扩展,例如:

// Public methods for building styles
public extension TextStyle {
    static func foregroundColor(_ color: Color) -> TextStyle {
        TextStyle(key: .init("TextStyleForegroundColor"), apply: { $0.foregroundColor(color) })
    }

    static func bold() -> TextStyle {
        TextStyle(key: .init("TextStyleBold"), apply: { $0.bold() })
    }
}
这里值得注意的是,NSAttributedString只是一个由范围上的属性注释的字符串。它不是一个带样式的字符串。我们可以根据需要设置任何属性键和值。因此,这些属性故意与Cocoa用于格式化的属性不同。

接下来,我们创建StyledText本身。我首先将重点放在此类型的&q;模型&部分上(稍后我们将使其成为视图)。

public struct StyledText {
    // This is a value type. Don't be tempted to use NSMutableAttributedString here unless
    // you also implement copy-on-write.
    private var attributedString: NSAttributedString

    private init(attributedString: NSAttributedString) {
        self.attributedString = attributedString
    }

    public func style<S>(_ style: TextStyle,
                         ranges: (String) -> S) -> StyledText
        where S: Sequence, S.Element == Range<String.Index>?
    {

        // Remember this is a value type. If you want to avoid this copy,
        // then you need to implement copy-on-write.
        let newAttributedString = NSMutableAttributedString(attributedString: attributedString)

        for range in ranges(attributedString.string).compactMap({ $0 }) {
            let nsRange = NSRange(range, in: attributedString.string)
            newAttributedString.addAttribute(style.key, value: style, range: nsRange)
        }

        return StyledText(attributedString: newAttributedString)
    }
}

它只是一个NSAttributedString的包装器,并且是通过将TextStyles应用于范围来创建新StyledTexts的一种方法。几个要点:

调用style不会改变现有对象。如果是这样的话,您就不能做return StyledText("text").apply(.bold())这样的事情了。您将收到一个错误,即该值是不可变的。

射程是棘手的事情。NSAttributedString使用NSRange,并且具有与字符串不同的索引概念。NSAttributedStrings的长度可以与基础字符串不同,因为它们组成字符的方式不同。

您不能安全地从一个字符串中获取String.Index并将其应用于另一个字符串,即使这两个字符串看起来完全相同。这就是为什么这个系统需要一个闭包来创建范围,而不是接受范围本身。attributedString.string与传入的字符串不完全相同。如果调用者想要传递Range<String.Index>,那么使用与TextStyle使用的字符串完全相同的字符串来构造它将是至关重要的。这是通过使用闭包最容易确保的,并且避免了大量的角例。

默认的style接口处理一系列范围以实现灵活性。但在大多数情况下,您可能只传递一个范围,所以对于需要整个字符串的情况,有一个方便的方法是很好的:

public extension StyledText {
    // A convenience extension to apply to a single range.
    func style(_ style: TextStyle,
               range: (String) -> Range<String.Index> = { $0.startIndex..<$0.endIndex }) -> StyledText {
        self.style(style, ranges: { [range($0)] })
    }
}

现在,创建StyledText的公共接口:

extension StyledText {
    public init(verbatim content: String, styles: [TextStyle] = []) {
        let attributes = styles.reduce(into: [:]) { result, style in
            result[style.key] = style
        }
        attributedString = NSMutableAttributedString(string: content, attributes: attributes)
    }
}
请注意此处的verbatim。此StyledText不支持本地化。可以想象,通过工作可以做到这一点,但还需要更多的思考。

最后,在所有这些之后,我们可以使它成为一个视图,方法是为每个具有相同属性的子字符串创建一个文本,将所有样式应用于该文本,然后使用+将所有文本合并为一个文本。为方便起见,文本直接显示,以便您可以将其与标准视图合并。

extension StyledText: View {
    public var body: some View { text() }

    public func text() -> Text {
        var text: Text = Text(verbatim: "")
        attributedString
            .enumerateAttributes(in: NSRange(location: 0, length: attributedString.length),
                                 options: [])
            { (attributes, range, _) in
                let string = attributedString.attributedSubstring(from: range).string
                let modifiers = attributes.values.map { $0 as! TextStyle }
                text = text + modifiers.reduce(Text(verbatim: string)) { segment, style in
                    style.apply(segment)
                }
        }
        return text
    }
}

就是这样。使用它时如下所示:

// An internal convenience extension that could be defined outside this pacakge.
// This wouldn't be a general-purpose way to highlight, but shows how a caller could create
// their own extensions
extension TextStyle {
    static func highlight() -> TextStyle { .foregroundColor(.red) }
}

struct ContentView: View {
    var body: some View {
        StyledText(verbatim: "‍‍someText1")
            .style(.highlight(), ranges: { [$0.range(of: "eTex"), $0.range(of: "1")] })
            .style(.bold())
    }
}

Gist

您也可以只将一个UILabel包装在一个UIView可表示的中,然后使用attributedText。但这将是作弊。:d

816