深入浅出 NSFetchResultsController

前言

网上看了很久,没发现有比我这篇更详细的。

如果学到或者查到 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 的强大之处。

你可能感兴趣的:(深入浅出 NSFetchResultsController)