了解了上一节基于 Swift KeyPath 处理 Core Data 记录之后,这一节,我们来看如何通过 NSFetchRequest 进一步细化从 Core Data 查询数据的方式。

ContentStore

首先,在 BoxueDataKit / DataLayer / Repositories / Persistence / ContentStore / ContentStore.swift 里,我们定义了一个叫做 ContentStore 的协议,它约束了为 App 提供内容的各种接口:

public protocol ContentStore {
  func loadRecentUpdates(count: Int) -> [Episode]
  func loadFavoriteEpisodes(count: Int) -> [Episode]
  func loadWatchLaterEpisodes(count: Int) -> [Episode]
  func loadFreeEpisodes(count: Int) -> [Episode]
  func loadWatchingHistories(count: Int) -> [Episode]
  func loadLearningPath(count: Int) -> [LearningPath]
}

就像这些方法的名字体现的,它们分别用于读取最近更新视频、用户收藏视频、稍后观看视频、免费视频、观看历史记录以及学习路径信息。而 count 参数,则表示要读取的记录数量。

CoreDataBasedContentStore

然后,还是在 ContentStore 目录中,我们创建了一个 CoreDataBasedContentStore.swift,它是基于 Core Data 的 ContentStore 实现。我们从提供核心功能的 load 方法说起:

/// Inside `CoreDataBasedContentStore`
private func load<T: NSManagedObject>(
  by predicate: NSPredicate? = nil,
  limit: Int? = nil,
  descriptors: [NSSortDescriptor]? = nil) -> [T] {
  let context = CoreDataManagerX.shared.mainManagedObjectContext
  /// Can not use `T.fetchRequest()` here.
  /// Either convert the `NSFetchRequest<NSFetchRequestResult>`
  /// to `NSFetchRequest<T>` explicitly or use the following `init`:
  let fetchRequest = NSFetchRequest<T>(entityName: String(describing: T.self))
  do {
    fetchRequest.predicate = predicate

    if let limit = limit {
      fetchRequest.fetchLimit = limit
    }

    fetchRequest.sortDescriptors = descriptors

    return try context.fetch(fetchRequest)
  }
  catch {
    return []
  }
}

其中:

  • 泛型参数 T 表示我们要读取记录的 ENTITY 对应的 NSManagedObject 对象;
  • predicate 是一个 NSPredicate 对象,我们通过它指定从 Core Data 查询记录时的条件。它类似 SQL 中的 WHERE 语句;
  • limit 表示最多读取的记录数;
  • descriptors 是一个 NSSortDescriptor 数组,在这里,我们可以指定多个对查询结果进行排序的条件;

load 的实现里,我们使用了 mainManagedObjectContext,因为这些方法都是为 UI 提供数据服务的。然后,用 NSFetchRequest<T>(entityName:) 方法,根据 T 的类型,创建了 NSFetchRequest 对象。这里,我们不能直接使用 T.fetchRequest() 方法,编译器无法明确 T 究竟是哪个具体的 NSManagedObject 派生类,只能用这个通过 entityName 参数明确指定具体的 ENTITY。

有了 fetchRequest 对象之后,我们通过设置它的 predicate 属性指定了查询条件。用 fetchLimit 属性指定了查询数量,用 sortDescriptors 属性,指定了查询结果的排序方法。这三部分都设置好之后,就可以把它传递给 context.fetch 方法查询记录了。

是不是很简单?从某种意义上说,它甚至比上一节我们实现的 load 方法还简单易懂。于是,接下来的问题就变成了,根据泊学 App 的显示需要,从 Core Data 中读取不同记录了。

例如,读取最近更新的视频,可以这样:

public func loadRecentUpdates(count: Int) -> [Episode] {
  return load(
    limit: count,
    descriptors: [NSSortDescriptor(key: "updatedAt", ascending: false)])
}

这里,我们没有指定查询条件,表示读取所有记录中的 count 个记录。descriptors 指定了对查询结果按照 updatedAt 字段降序排列。这样得到的,就是前 count 个最近更新的视频了。

接下来,再来看个指定查询条件的方法,例如,读取稍后再看的视频记录:

public func loadWatchLaterEpisodes(count: Int) -> [Episode] {
  let pred = NSPredicate(format: "watchLaterAt != nil")
  return load(
    by: pred,
    limit: count,
    descriptors: [NSSortDescriptor(key: "watchLaterAt", ascending: false)])
}

和刚才唯一的区别,就是我们定义了一个 NSPredicate 对象,在它的 format 参数里,直接使用 "watchLaterAt != nil" 这种形式定义了查询时的条件。除此之外,format 还支持 String Format Specifiers 以及 printf 中的格式化表达方式,如果你学过 C,肯定会对这些用法并不陌生。

例如,查询免费视频时,我们使用了 %i 表示查询整数:

public func loadFreeEpisodes(count: Int) -> [Episode] {
  let pred = NSPredicate(format: "type == %i", Episode.FREE_EPISODE)
  return load(
    by: pred,
    limit: count,
    descriptors: [NSSortDescriptor(key: "updatedAt", ascending: false)])
}

除了常用的格式替代符之外,我们还可以使用 %@%K。前者表示某个 Objective-C 对象的值,后者,表示某个 KeyPath。例如加载观看历史的方法:

public func loadWatchingHistories(count: Int) -> [Episode] {
  let pred = NSPredicate(format: "%K != nil && progress != 0", "watchedAt")
  return load(
    by: pred,
    limit: count,
    descriptors: [NSSortDescriptor(key: "watchedAt", ascending: false)])
}

这里,我们用 %K != nil 的形式,之后,给它传递了对应属性的 KeyPath,这和我们直接在 format 里写 watchedAt != nil 是一样的,大家了解这种用法就好了。

至于 CoreDataBasedContentStore 中其余的 load 方法,道理都是一样的,大家自己去看代码就好,我们就不一一重复了。最后,我们还定义了一个查询 App 是否有缓存的方法:

public func cacheExist() -> Bool {
  let context = CoreDataManagerX.shared.mainManagedObjectContext
  let fetchRequest = NSFetchRequest<NSNumber>(entityName: "Episode")
  do {
    fetchRequest.resultType = .countResultType
    let numbers = try context.fetch(fetchRequest)

    return !numbers.isEmpty && numbers[0] != 0
  }
  catch {
    return false
  }
}

当然,方法有点粗糙,只是简单判断了下 Episode 是否有记录,但对我们来说也足够有效就是了。这种设置 fetchRequest.resultType 的方法,之前我们说过,这里就不再重复了。它要比我们读取所有 Episode 记录到内存,然后再 count 经济多了。

What's next?

至此,我们为了缓存 App 内容浏览首页数据做的准备工作就全部完成了。下一节,我们来实现请求内容首页数据的 HTTP API。

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

¥ 59

按月订阅

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

开始订阅

¥ 512

按年订阅

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

开始订阅

¥ 1280

泊学终身会员

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

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