探索 Foundation 新增功能

356次阅读  |  发布于3年以前

引言

根据 Apple 开发者官网 WWDC21 - What's new in Foundation[2] 上的描述,iOS 以及 macOS 底层的系统核心库 Foundation 带来了如下更新:

一、属性字符串 - AttributedString

iOS 中的 AttributedString 相信大多数开发者都不会陌生,它由以下三部分组成:

AttributedString 允许您将属性(键值对)与字符串的特定范围相关联,最常见的字符串属性由 SDK 进行定义,但是你可以创建属于你自己的属性。

Foundation 框架面世之时,NSAttributedString 类型便随之一起推出。而今年,Apple 推出了一个基于现代化的 Swift 语言特性的全新的结构体 - AttributedString

1.1 AttributedString 初见

AttributedString 有以下特性:

1.2 AttributedString 基础

上面的截图中,有文字被加粗,也有文字是斜体,还有带链接可点击的文字。那么如何通过全新的 AttributedString 来实现呢?

同时,我们这里对 AttributedString 设置一个的 SwiftUI 字体属性 bold,设置之后会让字体有加粗的效果。值得注意的是,该属性作用在 AttributedString 全局范围内。

我们前面说到过,AttributedString 是由字符,区间以及属性字典三大部分组成的。但是如果需要访问一个 AttributedString 的这三大部分内容并不是直接通过 AttributedString 本身,而是通过 View

1.3 AttributedString View

AttributedStringView 由下列两部分组成:

ViewSwiftCollection 类型,那么也就是说我们可以像操作 Array 类型一样来操作 View

1.3.1 Characters View

现在假设设计师要求我们实现标点符号的文字颜色为橘色:

那么我们可以通过以下代码来实现:

1.3.2 Runs View

一个 Run View 包含了一个特定的属性的起始位置、长度、以及具体的属性值。

上面的字符串由 4 个 Run View 组成。分别是 Thank you!Please visit ourwebsite.

如上代码所示,我们可以通过 AttributedStringruns 属性下面的 count 来获取有多少个 Run ViewcharacterRun View 的区间是可以相互转换的,所以你可以通过下标访问到某个区间的字符串内容。

对于 Run View 来说,专注于一个特定的属性这种场景可能更加有用。

上面的代码中,我们使用了 linkkey path 来合并链接属性。如果我们只关注链接属性的话,我们这里有三个 Run View。遍历 Run View 会得到一个 (value, range) 的元组。因为 value 是类型安全的,所以我们可以使用存在于 URL 上的 scheme 这样的 API。上面的代码表达意思就是检查开头不为 https 的字符串,匹配成功则加入 insecureLinks 数组中。

1.4 AttributedString 使用

另外一个有用的场景是在 AttributedString 中查找特定的子串,查找成功后可以编辑对应区间内的字符串内容以及字符串属性。比如我们想把 visit 改成更为复古的 surf,如下图所示: 我们只需要下方代码即可实现:

1.5 AttributedString 本地化

Swift 中的 AttributedString 以及 Objective-C 中的 NSAttributedString 现在都已支持本地化。在本地化的场景下,AttributedString 就像普通的字符串一样位于你的 App 的字符串文件中。在 Swift 中,现在支持通过字符串插值对 StringAttributedString 进行本地化的格式化工作,就像 SwiftUI 中的 text view 一样。 如上图代码所示:

Xcode 可以通过编译器自动生成字符串文件,只需要开启位于 Build Settings 下的 Localization 设置中的 Use Compiler to Extract Swift Strings 选项。

1.6 AttributedString & Markdown

AttributedString 支持 Markdown 语法,下面是一个 SwiftUIText 组件使用本地化的 AttributedString 的例子:

Markdown 中的删除线以及代码块语法也得到了支持。

1.7 AttributedString 转换操作

要对 AttributedString 进行归档,我们需要能够在 AttributedStringNSAttributedString 这一引用类型之间进行相互转换。AttributedString 有可能是你的数据 model 中的一部分,因此,我们需要能够对其进行序列化和反序列化。 最后,我们希望能够在 Markdown 中设置自定义的字符串属性。

上述三种场景都涉及到了 AttributedString 的转换操作,我们依次进行分析。

1.7.1 从结构体类型转换为类类型

只需要将 AttribuedString 传入 NSAttributedString 的构造方法中即可,至于属性如何关联则交给 SDK 完成。

1.7.2 序列化和反序列化

因为 AttribuedString 有默认的 Codable 实现,这里的 Receipt 结构体只需要遵循 Codable 协议即可。

那么如何对自定义的字符串属性进行序列化和反序列化呢?我们可以对属性进行更深入的了解。一个属性由两部分组成:一个 key 和一个 valuekey 是一个遵循 AttributedStringKey 协议的类型,它定义了需要什么类型的值以及用于归档的属性名称。key 还可以遵循其他协议以自定义 value 的序列化和反序列化方式。

如上代码所示:AttributedStringKey 协议只需要定义关联类型 Value 以及静态的变量 name 即可。现在假设我们希望这个 RainbowAttribute 属性 Codable

Codable 其实是 Swift 标准库中的一个类型别名,它代表的是 DecodableEncodable 协议。

只需要像上面代码一样,让属性遵循 CodableAttributedStringKey 协议,该协议同样的也是 DecodableAttributedStringKeyEncodableAttributedStringKey 的类型别名。同时,让 Value 类型遵循 Codable 协议即可。

而如果想要上面的属性是我们的本地化字符串的一部分的话,只需要再遵循一个 MarkdownDecodableAttributedStringKey 协议即可。

声明一个遵循 MarkdownDecodableAttributedStringKey 协议的 AttributedString 属性,然后让属性的 value 部分遵循 Codable 协议,我们就可以在 Markdown 文本中实现富文本字符串的效果,具体使用参考下图。

上图中前两行代码在 Markdown 中十分常见,中括号中表示的是描述文本,括号中表示的是实际的 URL。带感叹号前缀的话就是直接渲染图片出来,不带感叹号前缀的话将会渲染一个链接出来。而第三行代码展示了自定义属性的语法。

自定义属性首先会以 ^ 开头,然后是一个中括号来接收文本,最后是一个括号来表示属性。属性以 JSON5格式表示。

JSON5[3] 与 JSON 兼容,并允许使用不带引号的 key、注释和一些其它功能。

Foundation 中的 JSON 相关的 API 也已经添加了对 JSON5 的支持。

因为自定义属性通过 JSON 进行表示,所以任何可以被 JSONDecoder 反序列化的内容自动与新的自定义 Markdown 语法兼容。

上图代码中,第一行包含了一个自定义属性,第二行包含了两个自定义属性,第三行包含了一个具有多个子属性的属性。

1.8 AttributedString Scopes

Scopes 是属性 key 的集合。Scopes 在反序列化 JSONMarkdown 时十分有用,因为它告诉了我们想要查找的属性,以及如何反序列化这些属性。

Apple 分别为 FoundationUIKitAppKitSwiftUI 定义了各自的 Scope。你可以定义属于你自己应用的 Scope

如上图所示,本地化字体文件中的 Markdown 在被转换为 AttributedString 后,应用会找到需要设置属性的范围并将属性应用到对应范围的字符串上。而因为属性来自于本地化字体文件,这适用于所有语言。

二、 格式化器 - Formatters

Formatters 有了全新的 API,作为 Foundation 框架另一个长期存在的功能,它们负责接收像数字、日期、时间等数据然后转换成本地化的,用户可读性更高的字符串。由于 Formatters 底层由相当多的配置数据支撑,所以缓存并重用 Formatters 已经成为一种常见的模式。但如果在由许多不同的代码间共享同一个 Formatter,并不总是合适的。今年,Apple 重新设计了 Formatter 相关 API,以提高性能与可用性。简而言之,新的 Formatters API 专注于「格式」上。

通过上面的示例代码,我们可以看到「缓存模式」在格式化器使用时的场景。这通常由两部分组成:

2.1 新的 Formatters 语法

那么可以简化上面的步骤吗?答案是可以的,在最新的 Formatters API 发布后,我们无需再手动创建并配置 Formatter,同时,我们也不需要传给 Formatter 一个日期对象了,我么只需要在日期对象身上调用 formatted 方法,并指定格式化标准是什么。仅仅一行代码就完成了上面两个步骤的工作。

dateLabel 的内容通过新的 Formatters API 转换后,我们不妨关注下面 magnitudeLabel 的内容。看起来一行代码就完成了从浮点数到字符串的格式化,但其实这种转换隐藏了一些复杂性,并且存在一些值得注意的陷阱,稍有不慎就会得到完全错误的结果。读者需要在转换浮点数到字符串时特别注意字符串常量的修饰符。

相反,上面的新 API 更容易理解,并且可读性,可维护性也更高。通过使用 Swift 中常规的函数来声明 我们希望 magnitude 这个浮点数如何被格式为一个字符串。同时新的 API 可以实现代码自动补全和类型安全。

Apple 将会在 Foundation 中所有的十个格式化器中统一应用新的 Formatters API,内容包括通过清理和简化接口,并底层代码重构以避免常见的错误,同时添加了一系列的新功能。

接下来,我们将深入两种最常见的格式化场景:日期与数字。

2.2 日期格式化

日期格式化本质上就是将一个绝对的时间点转换为我们人类所理解的日期,这其中又涉及到了「日历」以及「时区」。甚至更重要的是,还需要考虑到不同地区的人们对于日期显示的偏好不同,这种偏好我们一般称之为「语言环境」,即 locale。接下来一起来看一下最简单的日期格式化代码是怎样编写的吧。

默认的日期格式化就是如此简单,当然,就像上面例子中看得到那样,日期格式化支持多样化的配置。

新的 Formatters API 的一个重要的目标是在于创建正确的格式化时提供尽可能多的编译时支持。使用魔法字符串进行格式化会造成陷阱,这种陷阱在正常情况下看起来是正确的,但在极端情况下会产生完全错误的值。比方获取一年中的最后一天。

对于每一种类型来说,可能存在不止一种的样式。比如日期,就有 dateTimeISO 8601 两种显示样式。样式可用于默认配置或自定义。

本次更新对于格式化相互关联的两个日期也提供了新的 API

2.2.1 格式化AttributedString

Formatters 另外一个新的特性是针对 AttributedString 进行格式化。通过全新的结构体 AttributedString 以及 Formatters API,在任何地方都可以实现格式化 AttributedString 的输出。在 watchOS中,许多 complications 都是格式化的字符串,因为 Apple Watch 是十分个性化的设备,所以考虑到用户的偏好设置是十分重要的。下面通过 SwiftUI 中的 demo,我们可以一探究竟。

上图是 Caffe Companion 应用的起始点,它会展示你的下一杯免费咖啡。这里有一个仅用来显示格式化日期字符串 SwiftUI 视图。通过设置日期格式化器中的语言环境(locale)参数,可以在 SwiftUI preview 中调整不同的语言环境以预览不同的效果。

通过在 SwiftUI Preview 中添加不同的语言环境,我们可以测试出在不同语言环境下,虽然星期的显示位置不同,但是最终都正确的加上了橙色。

2.2.2 字符串转换为日期

上面的内容都是从日期转换为字符串,接下来我们来讨论从字符串转换为日期的场景。

2.3 数字格式化

所谓数字格式化就是将一个整数或者浮点数转换为一个人可以阅读的内容。

SwiftUI 支持在 TextFeild 上设置内容格式的样式,而因为格式的样式有需要被格式化内容的类型,所以我们可以使用一个可读的但是安全的语法实现对 tip 的格式化。

2.4 字符串和格式化器的国际化与本地化

关于字符串与格式化器的国际化以及本地化的更多内容可以参考本次 WWDC 的其他 Session:Localize your SwiftUI app[4] & Streamline your localized strings[5]

三、 语法协议引擎 - Grammar agreement

最后,我们将目光转移到一个全新的功能上 -- 自动修正语法(Automatic grammar agreement)。在之前,西班牙语等语言的本地化表达自然翻译的能力受到限制,有时会导致尴尬的对话出现。这些语言需要进行转换以实现在 不同的对话中达到时态与复数的一致,有时甚至需要了解用户的首选称呼。英语也具有同样的特性,名词具有单数和复数两种形式。

3.1 Automatic Grammar Agreement 初见

我们讨论了语言中的一些术语,接下来让我们以实际的例子进行更深入的讨论吧。

Caffe App 中,我们可以点餐,然后设置餐食的大小以及数量。我们先点一份沙拉。

接着我们的朋友说她也需要一份,所以我们就增加数量到两份。在英语中,salad 这个单词需要改变自己的单复数形式以匹配两份沙拉,这就叫做语法协议。这也就是说这句话中的所有单词必须相互匹配。在英语中,由于复数的问题而修正单词是一种常见的语法协议。现在切换我们的 App 到西班牙语。

这里我们点了一份 ensalada pequeña ,或者说一小份沙拉。当我们为朋友再点一份的时候,订单确认按钮需要与英语进行同样的复数化,但有一点是不同的。在西班牙语中,像「小的」这样的形容词以及「沙拉」这样的名词都需要和具体的数量达成一致。

所以,订单确认按钮上显示的是 ensaladas pequeñas 而不是 ensalada pequeña

我们接着讲目光锁定到饮品上,如上图所示,对于订单按钮中的文字来说,不仅需要单复数匹配正确,还需要在这些单词的词法性上达成一致。Juicejugo 是阳性化的。而形容词 pequeño 「小的」也必须匹配。为了正确对这些文字进行国际化和本地化,我们最终会遇到「组合爆炸」的问题。食物、大小和数量的每个组合都需要不同的本地化字符串。在代码层面,就会出现如下所示的场景。

我们需要对每个 item 进行 switch 操作,同时还需要对选择的大小进行判断,等等。还需要一个字符串文件来正确地对每个字符串中的计数进行复数化。而现在,通过利用在系统键盘上提示用户输入这一功能的相同技术,Apple 创建了一个新的 API,就可以轻松处理上面我们所遇到的问题了。此功能被命名为自动语法协议,因为系统会自动修复本地化字符串以使它们有正确的语法。

3.2 Automatic Grammar Agreement 解析

有了新的 Automatic Grammar Agreement 加持,代码变得更简单了。你可以在一个字符串里直接指定数 量、大小和具体的餐食。自动语法协议会通过「反射」自动修复其中的语法。让我们逐步分解一下,为了执行「反射」流程,我们需要知道字符串中的哪些部分需要做自动语法修复。幸运的是,在 Swift 中有一个全新的类型 AttributedString AttributedString,以及在 Markdown 中可以设置自定义属性的功能。当我们导出 Caffe 项目的本地化时,我们会得到一个字符串文件,这个文件中包含我们的提示文本以及代码中的本地化字符串,比如餐食的名称以及大小。

在拉丁美洲西班牙环境下,本地化器使用重新排列语法将大小和餐食的顺序进行了调换,这是因为西班牙语中像「小的」或「大的」这样的形容词位于名词的后面。有些语言不仅在本地化文本本身上,还在文本与阅读的人之间具有一致性。自动语法协议也有助于解决这一问题。

举个例子,如上图所示,iOS 系统自带的备忘录应用在第一次使用时会弹出一个欢迎菜单。在英语中,欢迎语是 Welcome to Notes,即欢迎使用备忘录。而在西班牙语中,则是 Te damos la bienvenida a Notas,即我们欢迎您使用备忘录。我们希望有和英语一样的西班牙语体验。然而,在西班牙语中,bienvenido 一词必须与用户首选的称号相匹配。称号可能是几个选项之一,而具体的选项就会更改文本的内容。使用正确的称号可以带来更个性化和更具包容性的体验。

在今年的更新当中,Apple 为西班牙语用户提供了设置了称号的入口。在语言与地区的设置中,将会有一个新的称号选项。

如上图所示,用户可以选择设置不同的称号并选择是否与所有 App 共享这一设置。

上图是用户设置了女性称谓后,新的备忘录欢迎界面。

而上图是设置了男性称谓后的备忘录欢迎界面。如果我们不知道用户是否有设置过称谓,我们还是会以初始的字符串作为备选。今年,Apple 对西班牙语和英语实现了自动语法协议功能。就像系统应用备忘录的欢迎界面一样,你也可以在自己的应用中采用同样的技术。

四、总结

Foundation 今年有许多强大的新功能,你可以从今天开始在你的 app 中使用它们。

参考资料

[1]WWDC21 - What's new in Foundation: https://developer.apple.com/videos/play/wwdc2021/10109/

[2]WWDC21 - What's new in Foundation: https://developer.apple.com/videos/play/wwdc2021/10109/

[3]JSON5: https://json5.org

[4]Localize your SwiftUI app: https://developer.apple.com/videos/play/wwdc2021/10220/

[5]Streamline your localized strings: https://developer.apple.com/videos/play/wwdc2021/10221/

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8