基于上一节说到的 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])
  }
}

sortsorted 的区别,就是前者执行原地排序,后者返回排序后的新数组,这和标准库的做法是相同的。因此,我们就用 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 读取出来的记录类型,也就是数组中元素的类型。因此,我们要求 TNSManagedObject 的派生类;
  • 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 的实现,和大家分享它的用法。

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们