美文网首页本地缓存iOS开发 技术集锦
深入浅出 NSFetchResultsController

深入浅出 NSFetchResultsController

作者: 扬仔360 | 来源:发表于2015-11-30 23:54 被阅读704次

    前言

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

    如果学到或者查到 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

        本文链接:https://www.haomeiwen.com/subject/oqyjhttx.html