了解了上一节基于 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。