AttributedString——不仅仅让文字更漂亮
在WWDC 2021上,苹果为开发者带来了有一个期待已久的功能——AttributedString,这意味着Swift开发人员不再需要使用基于Objective-C的NSAttributedString来创建样式化文本。本文将对其做全面的介绍并演示如何创建自定义属性。
如果想获得更好的阅读体验,请访问我的博客www.fatbobman.com
初步印象
AttributedString是具有单个字符或字符范围的属性的字符串。属性提供了一些特征,如用于显示的视觉风格、用于无障碍引导以及用于在数据源之间进行链接的超链接数据等。
下面的代码将生成一个包含粗体以及超链接的属性字符串。
代码语言:javascript复制var attributedString = AttributedString("请访问肘子的博客")let zhouzi = attributedString.range(of: "肘子")! // 获取肘子二字的范围(Range)attributedString[zhouzi].inlinePresentationIntent = .stronglyEmphasized // 设置属性——粗体let blog = attributedString.range(of: "博客")! attributedString[blog].link = URL(string: "https://www.fatbobman.com")! // 设置属性——超链接
image-20211007165456612
在WWDC 2021之前,SwiftUI没有提供对属性字符串的支持,如果我们希望显示具有丰富样式的文本,通常会采用以下三种方式:
•将UIKit或AppKit控件包装成SwiftUI控件,在其中显示NSAttributedString•通过代码将NSAttributedString转换成对应的SwiftUI布局代码•使用SwiftUI的原生控件组合显示
下面的文字随着SwiftUI版本的变化,可采取的手段也在不断地增加(不使用NSAttributedString):
image-20211006163659029
SwiftUI 1.0
代码语言:javascript复制 @ViewBuilder var helloView:some View{ HStack(alignment:.lastTextBaseline, spacing:0){ Text("Hello").font(.title).foregroundColor(.red) Text(" world").font(.callout).foregroundColor(.cyan) } }
SwiftUI 2.0
SwiftUI 2.0增强了Text的功能,我们可以将不同的Text通过
合并显示
var helloText:Text { Text("Hello").font(.title).foregroundColor(.red) Text(" world").font(.callout).foregroundColor(.cyan) }
SwiftUI 3.0
除了上述的方法外,Text添加了对AttributedString的原生支持
代码语言:javascript复制 var helloAttributedString:AttributedString { var hello = AttributedString("Hello") hello.font = .title.bold() hello.foregroundColor = .red var world = AttributedString(" world") world.font = .callout world.foregroundColor = .cyan return hello world } Text(helloAttributedString)
单纯看上面的例子,并不能看到AttributedString有什么优势。相信随着继续阅读本文,你会发现AttributedString可以实现太多以前想做而无法做到的功能和效果。
AttributedString vs NSAttributedString
AttributedString基本上可以看作是NSAttributedString的Swift实现,两者在功能和内在逻辑上差别不大。但由于形成年代、核心代码语言等,两者之间仍有不少的区别。本节将从多个方面对它们进行比较。
类型
AttributedString是值类型的,这也是它同由Objective-C构建的NSAttributedString(引用类型)之间最大的区别。这意味着它可以通过Swift的值语义,像其他值一样被传递、复制和改变。
NSAttributedString 可变或不可变需不同的定义
代码语言:javascript复制let hello = NSMutableAttributedString("hello")let world = NSAttributedString(" world")hello.append(world)
AttributedString
代码语言:javascript复制var hello = AttributedString("hello")let world = AttributedString(" world")hello.append(world)
安全性
在AttributedString中需要使用Swift的点或键语法按名称访问属性,不仅可以保证类型安全,而且可以获得编译时检查的优势。
AttributedString中基本不采用NSAttributedString如下的属性访问方式,极大的减少出错几率
代码语言:javascript复制// 可能出现类型不匹配let attributes: [NSAttributedString.Key: Any] = [ .font: UIFont.systemFont(ofSize: 72), .foregroundColor: UIColor.white,]
本地化支持
Attributed提供了原生的本地化字符串支持,并可为本地化字符串添加了特定属性。
代码语言:javascript复制var localizableString = AttributedString(localized: "Hello (Date.now,format: .dateTime) world",locale: Locale(identifier: "zh-cn"),option:.applyReplacementIndexAttribute)
Formatter支持
同为WWDC 2021中推出的新Formatter API全面支持了AttributedString类型的格式化输出。我们可以轻松实现过去无法完成的工作。
代码语言:javascript复制var dateString: AttributedString { var attributedString = Date.now.formatted(.dateTime .hour() .minute() .weekday() .attributed ) let weekContainer = AttributeContainer() .dateField(.weekday) let colorContainer = AttributeContainer() .foregroundColor(.red) attributedString.replaceAttributes(weekContainer, with: colorContainer) return attributedString}Text(dateString)
image-20211006183053713
更多关于新Formatter API同AttributedString配合范例,请参阅WWDC 2021新Formatter API:新老比较及如何自定义[1]
SwiftUI集成
SwiftUI的Text组件提供了对AttributedString的原生支持,改善了一个SwiftUI的长期痛点(不过TextField、TextEdit仍不支持)。
AttributedString同时提供了SwiftUI、UIKit、AppKit三种框架的可用属性。UIKit或AppKit的控件同样可以渲染AttributedString(需经过转换)。
支持的文件格式
AttributedString目前仅具备对Markdown格式文本进行解析的能力。同NSAttributedString支持Markdown、rtf、doc、HTML相比仍有很大差距。
转换
苹果为AttributedString和NSAttributedString提供了相互转换的能力。
代码语言:javascript复制// AttributedString -> NSAttributedStringlet nsString = NSMutableAttributedString("hello")var attributedString = AttributedString(nsString)// NSAttribuedString -> AttributedStringvar attString = AttributedString("hello")attString.uiKit.foregroundColor = .redlet nsString1 = NSAttributedString(attString)
开发者可以充分利用两者各自的优势进行开发。比如:
•用NSAttributedString解析HTML,然后转换成AttributedString调用•用AttributedString创建类型安全的字符串,在显示时转换成NSAttributedString
基础
本节中,我们将对AttributedString中的一些重要概念做介绍,并通过代码片段展示AttributedString更多的用法。
AttributedStringKey
AttributedStringKey定义了AttributedString属性名称和类型。通过点语法或KeyPath,在保证类型安全的前提进行快捷访问。
代码语言:javascript复制var string = AttributedString("hello world")// 使用点语法string.font = .calloutlet font = string.font // 使用KeyPathlet font = string[keyPath:.font]
除了使用系统预置的大量属性外,我们也可以创建自己的属性。例如:
代码语言:javascript复制enum OutlineColorAttribute : AttributedStringKey { typealias Value = Color // 属性类型 static let name = "OutlineColor" // 属性名称}string.outlineColor = .blue
我们可以使用点语法或KeyPath对 AttributedString、AttributedSubString、AttributeContainer以及AttributedString.Runs.Run的属性进行访问。更多用法参照本文其他的代码片段。
AttributeContainer
AttributeContainer是属性容器。通过配置container,我们可以一次性地为属性字符串(或片段)设置、替换、合并大量的属性。
设置属性
代码语言:javascript复制var attributedString = AttributedString("Swift")string.foregroundColor = .red var container = AttributeContainer()container.inlinePresentationIntent = .strikethroughcontainer.font = .captioncontainer.backgroundColor = .pinkcontainer.foregroundColor = .green //将覆盖原来的redattributedString.setAttributes(container) // attributdString此时拥有四个属性内容
替换属性
代码语言:javascript复制var container = AttributeContainer()container.inlinePresentationIntent = .strikethroughcontainer.font = .captioncontainer.backgroundColor = .pinkcontainer.foregroundColor = .greenattributedString.setAttributes(container)// 此时attributedString有四个属性内容 font、backgroundColor、foregroundColor、inlinePresentationIntent// 被替换的属性var container1 = AttributeContainer()container1.foregroundColor = .greencontainer1.font = .caption// 将要替换的属性var container2 = AttributeContainer()container2.link = URL(string: "https://www.swift.org")// 被替换属性contianer1的属性键值内容全部符合才可替换,比如continaer1的foregroundColor为.red将不进行替换attributedString.replaceAttributes(container1, with: container2)// 替换后attributedString有三个属性内容 backgroundColor、inlinePresentationIntent、link
合并属性
代码语言:javascript复制var container = AttributeContainer()container.inlinePresentationIntent = .strikethroughcontainer.font = .captioncontainer.backgroundColor = .pinkcontainer.foregroundColor = .greenattributedString.setAttributes(container)// 此时attributedString有四个属性内容 font、backgroundColor、foregroundColor、inlinePresentationIntentvar container2 = AttributeContainer()container2.foregroundColor = .redcontainer2.link = URL(string: "www.swift.org")attributedString.mergeAttributes(container2,mergePolicy: .keepNew)// 合并后attributedString有五个属性 ,font、backgroundColor、foregroundColor、inlinePresentationIntent及link // foreground为.red// 属性冲突时,通过mergePolicy选择合并策略 .keepNew(默认) 或 .keepCurrent
AttributeScope
属性范围是系统框架定义的属性集合,将适合某个特定域中的属性定义在一个范围内,一方面便于管理,另一方面也解决了不同框架下相同属性名称对应类型不一致的问题。
目前,AttributedString提供了5个预置的Scope,分别为
•foundation包含有关Formatter、Markdown、URL以及语言变形方面的属性•swiftUI可以在SwiftUI下被渲染的属性,例如foregroundColor、backgroundColor、font等。目前支持的属性明显少于uiKit和appKit。估计待日后SwiftUI提供更多的显示支持后会逐步补上其他暂不支持的属性。•uiKit可以在UIKit下被渲染的属性。•appKit可以在AppKit下被渲染的属性•accessibility适用于无障碍的属性,用于提高引导访问的可用性。
在swiftUI、uiKit和appKit三个scope中存在很多的同名属性(比如foregroundColor),在访问时需注意以下几点:
•当Xcode无法正确推断该适用哪个Scope中的属性时,请显式标明对应的AttributeScope
代码语言:javascript复制uiKitString.uiKit.foregroundColor = .red //UIColorappKitString.appKit.backgroundColor = .yellow //NSColor
•三个框架的同名属性并不能互转,如想字符串同时支持多框架显示(代码复用),请分别为不同Scope的同名属性赋值
代码语言:javascript复制attributedString.swiftUI.foregroundColor = .redattributedString.uiKit.foregroundColor = .redattributedString.appKit.foregroundColor = .red// 转换成NSAttributedString,可以只转换指定的Scope属性let nsString = try! NSAttributedString(attributedString, including: .uiKit)
•为了提高兼容性,部分功能相同的属性,可以在foundation中设置。
代码语言:javascript复制attributedString.inlinePresentationIntent = .stronglyEmphasized //相当于 bold
•swiftUI、uiKit和appKit三个Scope在定义时,都已经分别包含了foundation和accessibility。因此在转换时即使只指定单一框架,foundation和accessibility的属性也均可正常转换。我们在自定义Scope时,最好也遵守该原则。
代码语言:javascript复制let nsString = try! NSAttributedString(attributedString, including: .appKit)// attributedString中属于foundation和accessibility的属性也将一并被转换
视图
在属性字符串中,属性和文本可以被独立访问,AttributedString提供了三种视图方便开发者从另一个维度访问所需的内容。
Character和unicodeScalar视图
这两个视图提供了类似NSAttributedString的string属性的功能,让开发者可以在纯文本的维度操作数据。两个视图的唯一区别是类型不同,简单来说,你可以把ChareacterView看作是Charecter集合,而UnicodeScalarView看作是Unicode标量合集。
字符串长度
代码语言:javascript复制var attributedString = AttributedString("Swift")attributedString.characters.count // 5
长度2
代码语言:javascript复制let attributedString = AttributedString("hello