深入浅出 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 的强大之处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容

  • 1 前言 CoreData不仅仅是数据库,而是苹果封装的一个更高级的数据持久化框架,SQLite只是其提供的一种数...
    RichardJieChen阅读 2,972评论 2 2
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,016评论 4 62
  • 来自网络 序言 目前形势,参加到iOS队伍的人是越来越多,甚至已经到供过于求了。今年,找过工作人可能会更深刻地体会...
    用心在飞阅读 823评论 5 4
  • 要理解Block的实现,要先理解runtime,然而理解Runtime要先理解C语言的结构体(可见我基础是TM有多...
    Kiven_Berry阅读 446评论 0 0
  • int a ; a=10; 当我们第一个变量的时候,就是在内存条里找一个格命名为a,然后再将10赋值进去。 内存条...
    无语_ae67阅读 365评论 0 0