
概览
在上一篇博文 Swift 中强大的 Key Paths(键路径)机制趣谈(上)中,我们介绍了 Swift 语言中键路径机制的基础知识,并举了若干例子讨论了它的一些用武之地。

而在本文中我们将再接再厉,继续有趣的键路径大冒险,为 KeyPaths 画上一个圆满的句号。
在本篇博文中,您将学到如下内容:
- 以不变应万变:超越对象实例
- 可写(Writable)键路径
相信学完本系列两篇博文之后,小伙伴们定能在今后的撸码中对键路径了然于胸、驾轻就熟。
那还等什么呢?Let's go!!!;)
4. 以不变应万变:超越对象实例
虽然"进化后"的键路径语法糖"很好很强大",但键路径真正的威力来自于这样一个事实:即它们允许我们引用对象属性,却不必将其与任何特定实例相关联。
这是什么意思呢?
还拿之前那个音乐播放器 App 的"栗子"来说,假设现在我们正在开发显示歌曲列表的功能,为了在 UI 中配置 Song 模型数据的列表 UITableViewCell 子视图,我们使用如下所示的配置器(Configurator)结构:
swift
struct SongCellConfigurator {
func configure(_ cell: UITableViewCell, for song: Song) {
cell.textLabel?.text = song.name
cell.detailTextLabel?.text = song.artistName
cell.imageView?.image = song.albumArtwork
}
}
需要再次说明的是,上面代码并没有犯什么错。
不过秃头小码农们都知道,在该 App 中我们以类似通用的方式来呈现其它模型数据的可能性很大(除了 Song 以外,App 中的其它类型也会在列表中显示标题、副标题和图像等信息),因此让我们看看是否可以使用键路径"摧枯拉朽"般的强大能力来创建可以与任何数据模型一起使用的"共享"配置器实现。
首先,我们创建一个名为 CellConfigurator 的泛型结构。由于我们希望为不同的模型呈现不同的数据,因此我们为其提供一组基于键路径的属性,对应于我们要呈现的每种数据:
swift
struct CellConfigurator<Model> {
let titleKeyPath: KeyPath<Model, String>
let subtitleKeyPath: KeyPath<Model, String>
let imageKeyPath: KeyPath<Model, UIImage?>
func configure(_ cell: UITableViewCell, for model: Model) {
cell.textLabel?.text = model[keyPath: titleKeyPath]
cell.detailTextLabel?.text = model[keyPath: subtitleKeyPath]
cell.imageView?.image = model[keyPath: imageKeyPath]
}
}
上面这段代码的"美轮美奂"之处在于,我们现在可以使用以前相同的轻量级键路径语法糖轻松为每个模型特例化(specialize)通用 CellConfigurator 配置器了,如下所示:
swift
let songCellConfigurator = CellConfigurator<Song>(
titleKeyPath: \.name,
subtitleKeyPath: \.artistName,
imageKeyPath: \.albumArtwork
)
let playlistCellConfigurator = CellConfigurator<Playlist>(
titleKeyPath: \.title,
subtitleKeyPath: \.authorName,
imageKeyPath: \.artwork
)
虽然像标准库的很多方法(如 map 和 sorted )一样,我们可以使用闭包来实现 CellConfigurator;但是使用键路径我们能够实现更加温文尔雅、蛾眉曼睩般的语法,而且我们也不再需要任何配置器实体来处理每一种繁杂的模型实例了,何乐不为呢?
5. 可写(Writable)键路径
到目前为止,我们一直在使用键路径读取值。现在就让我们看看如何使用它们来动态写入值吧。
小伙伴们在许多不同代码库中都曾见过一种很常见的开发模式,类似于下面的示例 ------ 加载要在 ListViewController 中呈现的 Item 列表。当加载操作完成时,我们需要将已加载的所有 Item 赋值到视图控制器上的对应属性中去:
swift
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load { [weak self] items in
self?.items = items
}
}
}
对于上面这种"加载+呈现"的开发范式,会有两个问题:
- 类似(Item)的数据模型相同功能势必造成代码重复;
- 稍不留意就会造成引用循环(retain cycles);
仔细思考一下:既然我们在上面真正想要做的是获取传递给闭包的值、并将其赋值给视图控制器上的属性,不如直接将该属性的 setter 作为方法传递,这样不是更 Cool 吗?
为了实现这一点,让我们首先定义一个函数。该函数允许我们将任何可写(Writable)键路径转换成一个闭包,该闭包负责将相关值写入该键路径对应的属性中。

这一次,我们将使用 ReferenceWritableKeyPath 类型,因为我们希望将该功能限制为仅引用类型(否则,我们只能更改值的本地副本)能够使用。
我们给定一个对象和该对象的键路径,然后将捕获该对象作为弱引用,并在调用函数后写入与键路径匹配的属性,如下代码所示:
swift
func setter<Object: AnyObject, Value>(
for object: Object,
keyPath: ReferenceWritableKeyPath<Object, Value>
) -> (Value) -> Void {
return { [weak object] value in
object?[keyPath: keyPath] = value
}
}
有了上面的"将伯之助",我们现在可以按如下形式简化之前的代码了:
swift
class ListViewController {
private var items = [Item]() { didSet { render() } }
func loadItems() {
loader.load(then: setter(for: self, keyPath: \.items))
}
}
从上面简化后的实现可以看到:我们彻底摆脱了"弱引用之舞"(weak self dance)并且将代码简洁性提升到一个新级别。
值得一提的是,当类似上面的代码逻辑"升华"与更高级的函数概念(如函数组合 function composition )相结合时情况会更加美妙,因为我们现在可以将多个setter 和其它函数"休戚相关"的链接在一起了,棒棒哒!💯
总结
在本篇博文中,我们介绍了如何用键路径超越对象实例,特例化(specialize)数据模型;以及用可写键路径彻底摆脱"引用循环",让简化代码"一蹴而就"。
感谢观赏,再会!8-)