> 原文出处: http://chengway.in/post/ji-zhu/core-data-by-tutorials-bi-ji-san
今天继续来学习Raywenderlich家[《Core Data by Tutorials》](http://www.raywenderlich.com/store/core-data-by-tutorials)的第五章,本章将会聚焦在**NSFetchedResultsController**
* * *
## **Chapter 5: NSFetchedResultsController**
作者在开篇就提到了NSFetchedResultsController虽然是一个controller,但是他并不是一个view controller,因为他没有view。
按本章的目录梳理一下
### **一、Introducing the World Cup app**
本章要完成一个World Cup App,作者提供了一个基本的Start Project,快速浏览一下,原始数据保存在seed.json文件中。
### **二、It all begins with a fetch request...**
**NSFetchedResultsController**大概是可以看做对“NSFetchRequest获取结果”这一过程的一种封装。话不多说,直接上代码:
~~~
//1
let fetchRequest = NSFetchRequest(entityName: "Team")
//2
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.context,
sectionNameKeyPath: nil,
cacheName: nil)
//3
var error: NSError? = nil
if (!fetchedResultsController.performFetch(&error)) {
println("Error: \(error?.localizedDescription)")
}
~~~
> 前面介绍过,NSFetchRequest是可以高度定制化的,包括sort descriptors、predicates等
注意一下NSFetchedResultsController初始化需要的两个必要参数fetchRequest和context,第3步由之前的context来performFetch改为NSFetchedResultsController来performFetch,可以看做是NSFetchedResultsController接管了context所做的工作,当然NSFetchedResultsController不仅仅是封装performFetch,他更重要的使命是负责协调Core Data和Table View显示之间的同步。这样一来,你所需要做到工作就只剩下了提供各种定制好的NSFetchRequest给NSFetchedResultsController就好了。
除了封装了fetch request之外,NSFetchedResultsController内部有容器存储了fetched回来的结果,可以使用fetchedObjects属性或objectAtIndexPath方法来获取到。下面是一些提供的Data source方法:
~~~
func numberOfSectionsInTableView(tableView: UITableView) -> Int {
return fetchedResultsController.sections!.count
}
func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchedResultsController.sections![section] as NSFetchedResultsSectionInfo
return sectionInfo.numberOfObjects
}
~~~
sections 数组包含的对象实现了NSFetchedResultsSectionInfo代理,由他来提供title和count信息。接着来看configureCell
~~~
func configureCell(cell: TeamCell, indexPath: NSIndexPath) {
let team = fetchedResultsController.objectAtIndexPath(indexPath) as Team
cell.flagImageView.image = UIImage(named: team.imageName)
cell.teamLabel.text = team.teamName
cell.scoreLabel.text = "Wins: \(team.wins)"
}
~~~
这里没有专门的数组来保存数据,数据都存在fetched results controller中,并通过objectAtIndexPath来获取。
这里还要注意的一点就是**NSFetchedResultsController至少需要设置一个sort descriptor**,标准的fetch request是不需要的,但NSFetchedResultsController涉及到table view的操作,需要知道列表的排列顺序。这样就可以了,按名称排序:
~~~
let sortDescriptor = NSSortDescriptor(key: "teamName", ascending: true)
fetchRequest.sortDescriptors = [sortDescriptor]
~~~
### **三、Grouping results into sections**
参加世界杯的有亚、非、欧、大洋洲、南美,中北美及加勒比海等六大洲,球队需要按归属地(qualifyingZone)分类。qualifyingZone是Team实体的一个属性。用NSFetchedResultsController实现起来相当简单:
~~~
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.context,
sectionNameKeyPath: "qualifyingZone",
cacheName: nil)
~~~
按sectionNameKeyPath分组,使用起来还是相当灵活的
> sectionNameKeyPath takes a keyPath string. It can take the form of an attribute name such as “qualifyingZone” or “teamName”, or it can drill deep into a Core Data relationship, such as “employee.address.street”.
这里还有一点要特别注意:上面我们将NSSortDescriptor只设为teamName排序,而当使用sectionNameKeyPath为qualifyingZone就会报错,正确的方法是在刚才设置NSSortDescriptor的地方添加key为qualifyingZone的NSSortDescriptor实例:
~~~
let zoneSort = NSSortDescriptor(key: "qualifyingZone", ascending: true)
let scoreSort = NSSortDescriptor(key: "wins", ascending: false)
let nameSort = NSSortDescriptor(key: "teamName", ascending: true)
fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]
~~~
> If you want to separate fetched results using a section keyPath, the first sort descriptor’s attribute must match the key path’s attribute.
> 如果要按section keyPath分组,必须创建一个key为key path的NSSortDescriptor实例,并放在**第一位**
运行一下程序,发现每个分组内的球队先是按分数排序,然后才会按姓名,这是因为数组内对象的先后顺序和排序的优先级是相关的。
~~~
fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]
~~~
### **四、“Cache” the ball**
将球队分组的操作开销有时候并不小,32支球队或许不算什么,但上百万的数据呢,或许你会想到丢掉后台去操作,这样确实不会阻塞主线程UI,但分组在后台还是会花上很长时间,你还是要loading好久才能有结果,如果每次fetch都这样,的确是个头疼的问题。好的解决办法就是,只付出一次代价,之后每次可以重用这个结果。NSFetchedResultsController的作者已经想到这个问题了,为我们提供了cahing来解决,打开它就好了。
~~~
fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest,
managedObjectContext: coreDataStack.context,
sectionNameKeyPath: "qualifyingZone",
cacheName: "worldCup")
~~~
要特别记住的就是这里的**section cache** 与Core Data中的*persistent store*是完全独立的
> NSFetchedResultsController’s section cache is very sensitive to changes in its fetch request. As you can imagine, any changes—such as a different entity description or different sort descriptors—would give you a completely different set of fetched objects, invalidating the cache completely. If you make changes like this, you must delete the existing cache using deleteCacheWithName: or use a different cache name.
> section cache其实相当易变,要时刻注意
### **五、Monitoring changes**
最后一个特性,十分强大但容易被滥用,也被作者称为是双刃剑。首先NSFetchedResultsController可以监听result set中变化,并且通知他的delegate。你只需要使用他的delegate方法来刷新tableView就ok了。
> A fetched results controller can only monitor changes made via the managed object context specified in its initializer. If you create a separate NSManagedObjectContext somewhere else in your app and start making changes there, your delegate method won’t run until those changes have been saved and merged with the fetched results controller’s context.
> 说白了就是只能监听NSFetchedResultsController初始化传进来的context中的changes,其他不相关的context是监听不到的,除非合并到这个context中,这个在多线程中会用到。
下面是一个通常的用法
~~~
func controllerWillChangeContent(controller: NSFetchedResultsController!) {
tableView.beginUpdates()
}
func controller(controller: NSFetchedResultsController,
didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath!,
forChangeType type: NSFetchedResultsChangeType,
newIndexPath: NSIndexPath!) {
switch type {
case .Insert:
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
case .Delete:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
case .Update:
let cell = tableView.cellForRowAtIndexPath(indexPath) as TeamCell
configureCell(cell, indexPath: indexPath)
case .Move:
tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Automatic)
tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Automatic)
default:
break
}
}
func controllerDidChangeContent(controller: NSFetchedResultsController!) {
tableView.endUpdates()
}
~~~
这个代理方法会被反复调用,无论data怎么变,tableView始终与persistent store保持一致。
### **六、Inserting an underdog**
最后作者开了个小玩笑,摇动手机可以走后门加一支球队进来(比如中国队?)。其实完全是为了展示Monitoring changes的强大~