基于上一节说到的 KeyPath,我们来解决一个从 Core Data 中读取记录时遇到的实际问题:从 Core Data 的某个 ENTITY 中读出来的记录顺序未必是我们想要的,这时就需要对 fetch
方法的返回值做进一步处理。
例如,所有的 Episode
记录,我们希望按照 id
降序排列,就得这样:
func load() -> [Episode] {
let context = CoreDataManager.shared.managedContext
let fetchRequest = NSFetchRequest<Episode>(entityName: "Episode")
do {
var result = try context.fetch(fetchRequest)
result.sort { $0.id < $1.id }
return result
}
catch {
return []
}
}
又或者,我们希望让所有 Episode
记录按最后更新时间排序,就得这样:
func load() -> [Episode] {
let context = CoreDataManager.shared.managedContext
let fetchRequest =
NSFetchRequest<Episode>(entityName: "Episode")
do {
var result = try context.fetch(fetchRequest)
result.sort { $0.updatedAt > $1.updatedAt }
return result
}
catch {
return []
}
}
如果还有更多要排序元素的读取方式该怎么办呢?为每个要求都编写一个类似的 load
方法听上去就不是一个好主意。因为这些 load
方法的逻辑几乎都是一样的,只是进行比较的属性不同以及属性的比较规则不同。
于是,我们很自然就想到,是否可以通过泛型来泛化这些差异呢?答案当然是没问题,这个过程分成两部分。一部分是泛化从 Core Data 读取结果的过程;另一部分是泛化对结果的排序。最终,实现上一节一开始展示的效果:
load(by: \.updatedAt, criteria: >)
扩展 Sequence
先来泛化排序的过程。为此,我们可以给 Array
创建一个扩展,在这里,新定义了两个 sort
家族方法:
public mutating func sort<T: Comparable>(
by keyPath: KeyPath<Element, T>,
criteria: (T, T) -> Bool = { $0 < $1 }) {
sort { a, b in
criteria(a[keyPath: keyPath], b[keyPath: keyPath])
}
}
public func sorted<T: Comparable>(
by keyPath: KeyPath<Element, T>,
criteria: (T, T) -> Bool = { $0 < $1 }) -> [Element] {
return sorted { a, b in
criteria(a[keyPath: keyPath], b[keyPath: keyPath])
}
}
sort
和 sorted
的区别,就是前者执行原地排序,后者返回排序后的新数组,这和标准库的做法是相同的。因此,我们就用 sort
举例。它的泛型参数 T
表示要排序的属性的类型,因此,T
必须是一个实现了 Comparable
的类型。sort
的第一个参数,表示这个属性在 Element
中的 KeyPath
。第二个参数,表示基于 T
的比较标准,默认是升序比较。
理解了这个签名之后,实现就很简单了。基于 keyPath
读出 Element
中的属性,把它传递给 criteria
进行比较,把比较的结果传递给标准库内置的 sort
方法,就实现基于 Element
的某个属性排序数组的效果了。另外,这样做还有一个额外的好处,就是即便数组中的 Element
自身不支持比较,我们还是可以用这个重载的版本进行排序。
理解了 sort
的实现之后,sorted
的实现就很简单了,我们就不重复了。基于这种思路,我们可以还可以编写 map / filter
等常用的高阶函数,大家可以自己动手练习一下。
泛化的 load
有了泛化的 sort
,泛化的 load
就非常简单了:
private func load<T, Attr: Comparable>(
by keyPath: KeyPath<T, Attr>,
criteria: (Attr, Attr) -> Bool = { $0 < $1 })
-> [T] where T: NSManagedObject {
let context = CoreDataManager.shared.managedContext
let fetchRequest =
NSFetchRequest<T>(entityName: String(describing: T.self))
do {
var result = try context.fetch(fetchRequest)
result.sort(by: keyPath, criteria: criteria)
return result
}
catch {
return []
}
}
它的两个泛型参数:
T
表示从 Core Data 读取出来的记录类型,也就是数组中元素的类型。因此,我们要求T
是NSManagedObject
的派生类;Attr
表示T
中的某个要比较的属性的类型,因此,Attr
应该是Comparable
的;
load
方法的参数和 sort
是完全一样的。而读取数据的过程,也是和一开始非泛化版的 load
几乎相同。这样一来,我们就可以对从 Core Data 中读取出来的结果,基于任意属性和排序规则,进行排序了:
func loadEpisodes() -> [Episode] {
return load(by: \.id)
}
private func loadRecentUpdates() -> [Episode] {
return load(by: \.updatedAt, criteria: >)
}
What's next?
以上,就是通过 KeyPath 重定义 Core Data 查询结果的方法,它可以让我们访问 Core Data 的方式,更加 Swift。不过,这种方法最大的问题,就是它会一下子读取某个 ENTITY 的全部记录,然后再按照我们设定的条件去排序或筛选。如果 ENTITY 的记录数非常大,这种方法就不适用了。
无论是数量问题,还是你希望进一步细化从 Core Data 获取数据的条件,我们还是要依赖 Core Data 自身也提供的 API。本质上,就是构建一个更精细的 NSFetchRequest
对象,下一节,我们就用泊学 App 中读取内容信息的类 CoreDataBasedContentStore
的实现,和大家分享它的用法。