Core Data by tutorials 笔记(三)

今天继续来学习 Raywenderlich 家《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 初始化需要的两个必要参数 fetchRequestcontext,第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 的强大~


-EOF-

如果感觉此文对你有帮助,请随意打赏支持作者 😘

chengway

认清生活真相之后依然热爱它!

Subscribe to Talk is cheap, Show me the world!

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!