前言
网上看了很久,没发现有比我这篇更详细的。
如果学到或者查到 NSFetchedResultsController
,我猜50%大概你已经了解到了CoreData到底是有多么强大,Apple 设计了 CoreData 这么一整套这么完美的持久化框架,在减少代码量的同时,也极大程度上提高运行效率。
在我们的日常开发过程中,使用 Core Data 的时候经常经常会跟 TableView 打交道,为了更好的使得两者合作更加紧密,Apple 设计了 NSFetchedResultsController
。
NSFetchedResultsController
并不是 ViewController 的子类,它仅仅是一个 Controller,没有视图没有界面。
为了介绍它的伟大,举个例子你有 1000 条数据,1000个 Cell 来展示你的那些数据。如果你删除或者更新了一条数据,在数据库中就要执行相应操作,这个时候通过 NSFetchedResultsController 的 Delegate,会自动去更新那一条数据的UI,而且不是整体更新,而是针对那一条数据进行更新,极大的优化了执行效率。
而这一切,你“几乎”什么都不用做。
NSFetchedResultsController
的简单用法:
假设你的类已经加入了 CoreDataStack,如果没有的话看这里
1、数据展示
首先需要一个 iVar:
var fetchRequestsController : NSFetchedResultsController!
然后在 ViewDidLoad 或者根据你项目自己的架构或者封装来初始化:
super.viewDidLoad()
let fetchRequest = NSFetchRequest(entityName: "Team")
//给 FetchRequest 加入排序,FetchResultsController 的 FetchRequest 至少需要一个排序,不可为空
let sortDesctiptor = NSSortDescriptor(key: "teamName", ascending: true)
fetchRequest.sortDescriptors = [sortDesctiptor]
//FetchResultsController 就像是 CoreData 和 TableView 之间的助手一样,但是即便如此还是需要提供FetchRequest。
//初始化方法包含了四个参数,第一个就是 FetchRequest,第二个是上下文 Context,拿它来 executeFetchRequest,三四个参数是Optional,后面会详写。
fetchRequestsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
do {
try fetchRequestsController.performFetch()
} catch let error as NSError {
print("Error: \(error.localizedDescription)")
}
ViewDidLoad 之后还需要在 DataSource 中做相应配置:
extension ViewController: UITableViewDataSource {
//暂时先这么写,后面的一部分回用到 Section count 的相关。
func numberOfSectionsInTableView
(tableView: UITableView) -> Int {
return fetchRequestsController.sections!.count
}
func tableView(tableView: UITableView,
numberOfRowsInSection section: Int) -> Int {
let sectionInfo = fetchRequestsController.sections![section]
return sectionInfo.numberOfObjects
}
func tableView(tableView: UITableView,
cellForRowAtIndexPath indexPath: NSIndexPath)
-> UITableViewCell {
let cell =
tableView.dequeueReusableCellWithIdentifier(
teamCellIdentifier, forIndexPath: indexPath)
as! TeamCell
configureCell(cell, indexPath: indexPath)
return cell
}
func configureCell(cell: TeamCell, indexPath: NSIndexPath) {
let team = fetchRequestsController.objectAtIndexPath(indexPath) as! Team
cell.flagImageView.image = UIImage(named: team.imageName!)
cell.teamLabel.text = team.teamName
cell.scoreLabel.text = "Wins: \(team.wins!)"
}
}
大概看一下 Cell 的 configureCell 方法就根据实际项目需求去写。
另外我们发现一般的 FetchRequest 并不是不需要进行排序的,但是这个必须要把 NSSortDescription 传递给 fetchRequest.sortDescriptors,至少需要一个排序,不可为空。
2、修改数据
常规的修改数据就是三步,先拿到数据,然后做修改,然后提交Change,跟SVN管理代码一个逻辑:
extension ViewController: UITableViewDelegate {
func tableView(tableView: UITableView,
didSelectRowAtIndexPath indexPath: NSIndexPath) {
//常规的修改数据就是三步,先拿到数据,然后做修改,然后提交Change,跟SVN管理代码一个逻辑:
let team = fetchRequestsController.objectAtIndexPath(indexPath) as! Team
let wins = team.wins!.integerValue
team.wins = NSNumber(integer: wins+1)
//创建 FRC 的时候是不是用 coreDataStack.context 创建的嘛,所以保存还是使用 coreDataStack.context 的这个方法。
coreDataStack.saveContext()
tableView.reloadData()
}
}
其实 NSFetchedResultsController 不仅仅便利了查询数据的便捷,这里因为它取出来的 Object 也和之前一样都是 ManagedObject,所以存数据也就跟以前一样。
这里最后使用了 tableView.reloadData() 来强制 UI 刷新。
3、Section分组显示 -- 初始化的时候的一个参数
在 NSFetchedResultsController 的初始化方法里的第三个参数,sectionNameKeyPath:
使用它,可以说明按照一个具体的 KeyPath 指向的属性可以对 FetchResults 进行分组。
//初始化方法包含了四个参数,第一个就是 FetchRequest,第二个是上下文 Context,拿它来 executeFetchRequest,三四个参数是Optional,后面会详写。
fetchRequestsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: coreDataStack.context, sectionNameKeyPath: nil, cacheName: nil)
keyPath 所指向的属性的每一个结果值都会产生一个分组,比如大家的这个属性都一样的话就是一个 Section,多少种结果就是多少种 Section。
添加一下 tableView 的 DataSource 方法 titleForHeaderInSection:
func tableView(tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
let sectionInfo = fetchRequestsController.sections![section]
return sectionInfo.name
}
但是做到这里还是不够,Section 来分配组的方法仅仅是根据数量,也就是说它只是知道前3个是一组,然后后面4个是一组,后面2个是一组。也就是说你得自己去实现排序的逻辑。
然后之前的排序是按照首字母来进行的,现在得自己来实现根据属性来排序,这样做,Section 的数量和 排序就能对应,就可以实现根据特定属性分组显示了。
所以还得去之前声明 NSSortDescriptor 的地方替换代码:
把之前写的下面的代码:
//给 FetchRequest 加入排序,FetchResultsController 的 FetchRequest 至少需要一个排序,不可为空
let sortDesctiptor = NSSortDescriptor(key: "teamName", ascending: true)
fetchRequest.sortDescriptors = [sortDesctiptor]
替换为
let nameSort = NSSortDescriptor(key: "teamName", ascending: true)
let zoneSort = NSSortDescriptor(key: "qualifyingZone", ascending: true)
let scoreSort = NSSortDescriptor(key: "wins", ascending: false)
fetchRequest.sortDescriptors = [zoneSort, scoreSort, nameSort]
这里传进去的是一个数组,这个数组的顺序就是排序规则的优先级。还有上面的排序规则,ScoreSort 的 ascending 是 false,代表着降序。
4、分组的优化 -- 初始化的时候的最后一个参数
话说回来,这个分组并不是一个低功耗的算法,还是会把每一个元素都循环遍历一遍。
话再回来,如果你开发的程序是对你司2万名员工的上下班打卡信息的展示,这样海量数据的展示,再用这种方法来进行分组可能就会出现问题。
还记不记得上面初始化 NSFetchedResultsController 的时候一共有四个参数,其实第四个的名字叫做 cacheName
,缓存名。你让 NSFetchedResultsController 开启在硬盘上的缓存,这个缓存跟 CoreData 中存储这次展示数据的 PersistentStore 是完全分离开的。
其实哦,已经保存的 Cache 对 FetchRequest 的变动非常敏感,比如EntityDescription、SortDescriptor 的变动都会给你两组完全不同的数据,数据都不同了,cache当然也完全不相同了。避免这种情况产生,当改变了FetchRequest的这些值,使用方法 deleteCacheWithName:
去删了Cache,或者用不同的CacheName。
所以打开缓存其实很简单,把第四个参数nil,换成具体的String就行了,但是还是慎用。
NSFetchedResultsController 的作用并不针对效率,所以更多跟效率有关的后面再研究。
5、监听
这部分的东西又是双刃剑,用的好很厉害,用不好就是自残。
最牛逼的一个点儿,就是 NSFetchedResultsController 其实是可以监听结果集 resultsSet 的变动,然后做一个通知,NSFetchedResultsControllerDelegate
,使用这个Delegate。
既然它能监听结果集的变动,那意思就是它能监听任何一个,任何它能抓到的对象,就是 fetchRequest 能到达的地方,它都能监听。
除一种情况,多重 Context 指向同一个持久层的情况它应该是监听不了的,毕竟 Context 的情急比它高。
初始化的时候加代理:
fetchRequestsController.delegate = self
用到的类加extension:
extension ViewController: NSFetchedResultsControllerDelegate {
}
代理中三个方法介绍:
func controllerWillChangeContent(controller: NSFetchedResultsController);
func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?);
func controllerDidChangeContent(controller: NSFetchedResultsController);
第一个方法即将开始改变的时候。
第二个方法是要告诉你具体哪个对象要改变,具体要做什么改变,增删改还是重新排序,以及受影响的模块是谁。这个方法紧紧连接着 tableView 和 CoreData.
第三个跟第一个类似,结束的时候调用。
然后还有一个代理方法可能会用到,如果你的分组改边了,你的Section进行了改变:
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType);
具体是什么时候调用第四个方法,比如你有四组数据,当你添加或者删除的时候,数据变成了五组数据或者三组数据,那么这个方法就会被调用。
6、插入数据
插入数据其实很简单,因为前面第五点已经实现了,所以插入数据只需要一个方法:
let newEntity = NSEntityDescription.insertNewObjectForEntityForName("YourEntity", inManagedObjectContext: self.coreDataStack.context) as! YourEntity
self.coreDataStack.saveContext()
因为监听一直存在,所以UI上的数据实时更新,这就是 NSFetchedResultsController 的强大之处。