SwiftUI:是不是存在修饰符来突出显示 Text() 视图的子字符串?
Posted
技术标签:
【中文标题】SwiftUI:是不是存在修饰符来突出显示 Text() 视图的子字符串?【英文标题】:SwiftUI: is there exist modifier to highlight substring of Text() view?SwiftUI:是否存在修饰符来突出显示 Text() 视图的子字符串? 【发布时间】:2019-12-20 13:52:30 【问题描述】:我在屏幕上有一些文字:
Text("someText1")
是否可以在不创建大量文本项的情况下突出显示/选择部分文本
我是说
Text("som") + Text("eTex").foregroundColor(.red) + Text("t1")
对我来说不是解决方案
最好有某种修饰符以某种方式突出显示文本的一部分。类似于:
Text("someText1").modifier(.highlight(text:"eTex"))
有可能吗? (我的意思是没有创建很多视图)
【问题讨论】:
【参考方案1】:一旦你创建了一个文本,你就不能再打开它了。您的示例会产生本地化问题。 someText1
实际上不是要打印的字符串。它是字符串的本地化键。默认的本地化字符串恰好是键,所以它可以工作。当您进行本地化时,您搜索eTex
的尝试会悄然中断。所以这不是一个好的通用接口。
即便如此,构建解决方案还是很有启发性的,并且可能对特定情况有用。
基本目标是将样式视为应用于范围的属性。这正是 NSAttributedString 为我们提供的,包括合并和拆分范围以管理多个重叠属性的能力。 NSAttributedString 对 Swift 不是特别友好,因此从头开始重新实现它可能会有一些价值,但我只是将其作为实现细节隐藏起来。
所以 TextStyle 将是一个 NSAttributedString.Key 和一个将 Text 转换为另一个 Text 的函数。
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 本身。我首先关注这种类型的“模型”部分(稍后我们会将其设为视图)。
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,并且具有与 String 不同的索引概念。 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 不支持本地化。可以想象,通过工作可以做到,但需要更多的思考。
最后,毕竟,我们可以通过为每个具有相同属性的子字符串创建一个文本,将所有样式应用于该文本,然后使用 +
将所有文本组合成一个视图,从而使其成为一个视图.为方便起见,Text 直接公开,因此您可以将其与标准视图结合使用。
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 包装在 UIViewRepresentable 中,然后使用 attributedText
。但这就是作弊。 :D
【讨论】:
这真是太棒了。感谢分享! Rob,我很想听听您对我们如何添加对类似于 NSBackgroundColorAttributeName 的样式的支持的想法。将 .background() 修饰符应用于 Text 的挑战在于返回类型变为 View。您的实现依赖于将样式应用于 Text 将返回 Text 的事实,而应用 .background 修饰符时不会出现这种情况。我不认为我们可以将 View 打包回 Text,但我很想听听您的想法。谢谢! 我可以投票多少次?只有1个? :( “你也可以在 UIViewRepresentable 中包装一个 UILabel,然后使用属性文本。但这就是作弊。 :D” - 我可以问一下为什么会作弊吗?【参考方案2】:免责声明:我真的不愿意发布我的答案,因为我确信肯定有很多更聪明、更好的方式(我不知道可能是使用 TextKit 的 UIKit 视图的包装器) 和更强大的方法,但是......我认为这是一个有趣的练习,也许有人真的可以从中受益。
所以我们开始:
我将创建一个视图来保存一个字符串(用于渲染)和另一个视图来保存我们的“匹配”文本,而不是一个修饰符。
struct HighlightedText: View
let text: String
let matching: String
init(_ text: String, matching: String)
self.text = text
self.matching = matching
var body: some View
let tagged = text.replacingOccurrences(of: self.matching, with: "<SPLIT>>\(self.matching)<SPLIT>")
let split = tagged.components(separatedBy: "<SPLIT>")
return split.reduce(Text("")) (a, b) -> Text in
guard !b.hasPrefix(">") else
return a + Text(b.dropFirst()).foregroundColor(.red)
return a + Text(b)
我猜代码是不言自明的,但简而言之:
-
查找所有匹配项
用硬编码的“标签”替换它们(用另一个硬编码字符标记匹配的开始)
标签分割
如果我们在比赛中,减少组件并返回一个风格化的版本
现在,我们可以像这样使用它:
struct ContentView: View
@State var matching: String = "ll"
var body: some View
VStack
TextField("Matching term", text: self.$matching)
HighlightedText("Hello to all in this hall", matching: self.matching)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
这是一个(蹩脚的)gif,展示了它的实际效果:
https://imgur.com/sDpr0Ul
最后,如果您想知道我是如何在 Xcode 之外运行 SwiftUI,here is a gist 我已经在 Mac 上的 SwiftUI 中快速进行原型设计
【讨论】:
如何添加背景颜色(荧光笔效果)而不是只为文本着色?如果我添加背景,它不再被认为是视图? @simibac 很好的观察...据我所知,这在纯 SwiftUI 中可能实际上是不可能的(还)。我想有人不得不依赖 UIKit 并将其包装成一个可表示的对象。 看起来很酷的解决方案!我正在尝试对 TextField() 视图中的文本进行同样的突出显示。你认为这有什么可能吗? @Lars 由于此方法依赖于样式化的Text
连接,而TextField
没有提供由Text
初始化的方法,我看不出它是如何完成的(但我很高兴错了,因为我也想知道它是否真的可以通过任何其他方式)
@PeterKreinz 你是对的,它不是。在此示例中,字符串匹配部分是最简单的,但我想有人可以轻松修改它并使用正则表达式搜索/替换。【参考方案3】:
我非常喜欢@Alladinian 的简单解决方案,但我需要一个不区分大小写的解决方案,例如用于突出显示输入的字符。
这是我使用正则表达式的修改:
struct HighlightedText: View
let text: String
let matching: String
let caseInsensitiv: Bool
init(_ text: String, matching: String, caseInsensitiv: Bool = false)
self.text = text
self.matching = matching
self.caseInsensitiv = caseInsensitiv
var body: some View
guard let regex = try? NSRegularExpression(pattern: NSRegularExpression.escapedPattern(for: matching).trimmingCharacters(in: .whitespacesAndNewlines).folding(options: .regularExpression, locale: .current), options: caseInsensitiv ? .caseInsensitive : .init()) else
return Text(text)
let range = NSRange(location: 0, length: text.count)
let matches = regex.matches(in: text, options: .withTransparentBounds, range: range)
return text.enumerated().map (char) -> Text in
guard matches.filter(
$0.range.contains(char.offset)
).count == 0 else
return Text( String(char.element) ).foregroundColor(.red)
return Text( String(char.element) )
.reduce(Text("")) (a, b) -> Text in
return a + b
例子:
struct ContentView: View
@State var matching: String = "he"
var body: some View
VStack
TextField("Matching term", text: self.$matching)
.autocapitalization(.none)
HighlightedText("Hello to all in this hall", matching: self.matching, caseInsensitiv: true)
.font(.largeTitle)
.frame(maxWidth: .infinity, maxHeight: .infinity)
【讨论】:
【参考方案4】:Rob 解决方案的一个稍微不那么惯用的变体,它使用现有的 NSAttributedString 键(如果您已经有生成 NSAttributedString 的代码,则很有用)。这只是处理字体和前景颜色,但您可以添加其他。
问题:我想为某些文本元素添加链接,但我不能这样做,因为一旦通过点击手势(或替换为链接)修改了文本,它就不再可以与其他文本组合价值观。有没有一种惯用的方法?
extension NSAttributedString.Key
func apply(_ value: Any, to text: Text) -> Text
switch self
case .font:
return text.font(Font(value as! UIFont))
case .foregroundColor:
return text.foregroundColor(Color(value as! UIColor))
default:
return text
public struct TextAttribute
let key: NSAttributedString.Key
let value: Any
public struct AttributedText
// This is a value type. Don't be tempted to use NSMutableAttributedString here unless
// you also implement copy-on-write.
private var attributedString: NSAttributedString
public init(attributedString: NSAttributedString)
self.attributedString = attributedString
public func style<S>(_ style: TextAttribute,
ranges: (String) -> S) -> AttributedText
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)
let nsRange = NSRange(range, in: attributedString.string)
newAttributedString.addAttribute(style.key, value: style, range: nsRange)
return AttributedText(attributedString: newAttributedString)
public extension AttributedText
// A convenience extension to apply to a single range.
func style(_ style: TextAttribute,
range: (String) -> Range<String.Index> = $0.startIndex..<$0.endIndex ) -> AttributedText
self.style(style, ranges: [range($0)] )
extension AttributedText
public init(verbatim content: String, styles: [TextAttribute] = [])
let attributes = styles.reduce(into: [:]) result, style in
result[style.key] = style
attributedString = NSMutableAttributedString(string: content, attributes: attributes)
extension AttributedText: 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
text = text + attributes.reduce(Text(verbatim: string)) segment, attribute in
return attribute.0.apply(attribute.1, to: segment)
return text
public extension Font
init(_ font: UIFont)
switch font
case UIFont.preferredFont(forTextStyle: .largeTitle):
self = .largeTitle
case UIFont.preferredFont(forTextStyle: .title1):
self = .title
case UIFont.preferredFont(forTextStyle: .title2):
self = .title2
case UIFont.preferredFont(forTextStyle: .title3):
self = .title3
case UIFont.preferredFont(forTextStyle: .headline):
self = .headline
case UIFont.preferredFont(forTextStyle: .subheadline):
self = .subheadline
case UIFont.preferredFont(forTextStyle: .callout):
self = .callout
case UIFont.preferredFont(forTextStyle: .caption1):
self = .caption
case UIFont.preferredFont(forTextStyle: .caption2):
self = .caption2
case UIFont.preferredFont(forTextStyle: .footnote):
self = .footnote
default:
self = .body
【讨论】:
以上是关于SwiftUI:是不是存在修饰符来突出显示 Text() 视图的子字符串?的主要内容,如果未能解决你的问题,请参考以下文章
隐藏 SwiftUI DisclosureGroup 箭头并删除默认填充