2.0 连接模型到视图 (Core Data Programming Guide翻译)

这是苹果官方文档 Core Data Programming Guide 的渣翻译。

在OS X系统中,Core Data设计目的在于通过Cocoa bindings在用户接口发挥作用。然而,Cocoa bindings并不是iOS用户接口的一部分。在iOS中,你需要使用NSFetchedResultsController去连接模型(Core Data)和视图(storyboard)。

NSFetchedResultsController提供了Core Data和UITableView对象之间的接口。因为table视图是在iOS中用得最多的用以展示数据的方式,UITableView几乎处理了所有大量数据集合的显示。

创建一个Fetched Result Controller

一般来讲,一个NSFetchedResultsController实例会被一个UITableViewController实例在需要的时候初始化。这个初始化过程可以发生在viewDidLoad或者viewWillAppear方法,或者在其他视图控制器任意一个生命周期中某个时间点。下面这个例子展示了NSFetchedResultsController的初始化。

OBJECTIVE-C

@property (nonatomic, strong) NSFetchedResultsController *fetchedResultsController;
 
- (void)initializeFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
 
    NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
 
    [request setSortDescriptors:@[lastNameSort]];
 
    NSManagedObjectContext *moc = …; //Retrieve the main queue NSManagedObjectContext
 
    [self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:nil cacheName:nil]];
    [[self fetchedResultsController] setDelegate:self];
 
    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
        NSLog(@"Failed to initialize FetchedResultsController: %@\n%@", [error localizedDescription], [error userInfo]);
        abort();
    }
}

SWIFT

var fetchedResultsController: NSFetchedResultsController!
 
func initializeFetchedResultsController() {
    let request = NSFetchRequest(entityName: "Person")
    let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
    let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
    request.sortDescriptors = [departmentSort, lastNameSort]
    
    let moc = self.dataController.managedObjectContext
    fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")
    fetchedResultsController.delegate = self
    
    do {
        try fetchedResultsController.performFetch()
    } catch {
        fatalError("Failed to initialize FetchedResultsController: \(error)")
    }
}

在上述的、在UITableViewController实例运行的时候能一直存活的initializeFetchedResultsController方法中,你第一步构建了一个Fetch请求(NSFetchRequest),Fetch请求是这个NSFetchedResultsController的中心。注意这个Fetch请求包含了一个排序描述器(NSSortDescriptor)。NSFetchedResultsController需要至少一个排序描述器来控制展示出来的数据顺序。

一旦Fetch请求被初始化,你就可以初始化NSFetchedResultsController实例了。Fetched Results Controller需要你传递一个NSFetchRequest实例和一个托管对象上下文实例(NSManagedObjectContext)来运行操作。sectionNameKeyPath和cacheName属性都是可选的。

一旦Fetched Results Controller初始化了,你就可以设置它的代理(delegate)。这个代理会通知table view有关任何的数据结构的变化。一般来说,table view同时也是Fetched Results Controller的代理,所以能够在相关数据发生变化的时候调用回调。

下一步,你可以开始调用performFetch:来使用NSFetchedResultsController。这个调用会查询初始数据用以展示,并触发NSFetchedResultsController实例开始监控托管对象上下文MOC的变化。

集成Fetched Results Controller 和 Table View Data Source

在你集成了已初始化的fetched results controller和准备好了要展示在table view上的数据之后,你需要集成fetched results controller和table view的data source(UITableViewDataSource)。

OBJECTIVE-C

#pragma mark - UITableViewDataSource
 
- (void)configureCell:(id)cell atIndexPath:(NSIndexPath*)indexPath
{
    id object = [[self fetchedResultsController] objectAtIndexPath:indexPath];
 
    // Populate cell from the NSManagedObject instance
}
 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    id cell = [tableView dequeueReusableCellWithIdentifier:CellReuseIdentifier];
    // Set up the cell
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return [[[self fetchedResultsController] sections] count];
}
 
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    id< NSFetchedResultsSectionInfo> sectionInfo = [[self fetchedResultsController] sections][section];
    return [sectionInfo numberOfObjects];
}

SWIFT

func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
    let employee = fetchedResultsController.objectAtIndexPath(indexPath) as! AAAEmployeeMO
    // Populate cell from the NSManagedObject instance
    print("Object for configuration: \(object)")
}
 
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCellWithIdentifier("cellIdentifier", forIndexPath: indexPath) as! UITableViewCell
    // Set up the cell
    configureCell(cell, indexPath: indexPath)
    return cell
}
 
override func numberOfSectionsInTableView(tableView: UITableView) -> Int {
    return fetchedResultsController.sections!.count
}
 
override func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let sections = fetchedResultsController.sections as! [NSFetchedResultsSectionInfo]
    let sectionInfo = sections[section]
    return sectionInfo.numberOfObjects
}

就如上面每个UITableViewDataSource的方法展示的那样,fetched results controller的集成接口减少到了只需要一个就能跟table view data source进行集成了。

传递数据更新到Table View

除了更加容易集成Core Data和table view data source之外,NSFetchedResultsController还能在数据发生改变的时候跟UITableViewController实例通信。为了实现这个功能,需要实现 NSFetchedResultsControllerDelegate协议:

OBJECTIVE-C

#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
    [[self tableView] beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [[self tableView] insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [[self tableView] deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeMove:
        case NSFetchedResultsChangeUpdate:
            break;
    }
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath
{
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete:
            [[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[[self tableView] cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
            break;
        case NSFetchedResultsChangeMove:
            [[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
            [[self tableView] insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
            break;
    }
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [[self tableView] endUpdates];
}

SWIFT

func controllerWillChangeContent(controller: NSFetchedResultsController) {
    tableView.beginUpdates()
}
 
func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    switch type {
    case .Insert:
        tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    case .Delete:
        tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    case .Move:
        break
    case .Update:
        break
    }
}
 
func controller(controller: NSFetchedResultsController, didChangeObject anObject: NSManagedObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    switch type {
    case .Insert:
        tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
    case .Delete:
        tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
    case .Update:
        configureCell(self.tableView.cellForRowAtIndexPath(indexPath!)!, indexPath: indexPath!)
    case .Move:
        tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
        tableView.insertRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
    }
}
 
func controllerDidChangeContent(controller: NSFetchedResultsController) {
    tableView.endUpdates()
}

实现这4个上述的协议方法就能够自动更新UITableView,无论何时相关数据的更新都能够触发更新。

添加Sections

到目前为止你可以已经使用一个仅有一个sectiion的table view,这个setion展示了所有table view需要展示的数据。如果你要操作拥有更多数据的Employee对象集合,那么把这个table view分成多个section更好。把Employee按照department来分组更加利于管理。如果不使用Core Data,实现一个带有多个section的table view可以解析一个含有多个数组的数组或更加复杂的数据结构。有了Core Data,你仅需要对fetched results controller的构建做出一点小修改即可.

OBJECTIVE-C

- (void)initializeFetchedResultsController
{
    NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Person"];
    NSSortDescriptor *departmentSort = [NSSortDescriptor sortDescriptorWithKey:@"department.name" ascending:YES];
    NSSortDescriptor *lastNameSort = [NSSortDescriptor sortDescriptorWithKey:@"lastName" ascending:YES];
    [request setSortDescriptors:@[departmentSort, lastNameSort]];
    NSManagedObjectContext *moc = [[self dataController] managedObjectContext];
    [self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:nil]];
    [[self fetchedResultsController] setDelegate:self];

SWIFT

func initializeFetchedResultsController() {
    let request = NSFetchRequest(entityName: "Person")
    let departmentSort = NSSortDescriptor(key: "department.name", ascending: true)
    let lastNameSort = NSSortDescriptor(key: "lastName", ascending: true)
    request.sortDescriptors = [departmentSort, lastNameSort]
    let moc = dataController.managedObjectContext
    fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: nil)
    fetchedResultsController.delegate = self
    do {
        try fetchedResultsController.performFetch()
    } catch {
        fatalError("Failed to initialize FetchedResultsController: \(error)")
    }
}

在这个例子中你需要添加多一个NSSortDesctiptor实例到NSFetchRequest实例中。你要在NSFetchedResultsController的初始化中为sectionNameKeyPath参数设置跟这个新sort descriptor同样的key。fetched results controller使用这个初始化的controller并把数据分成多个section,因此所有的key都要匹配。

这个修改让fetched results controller把返回的Person实例基于每个Person相关的department的name分成了多个section。唯一使用这个特性的条件是:

  • sectionNameKeyPath属性必须是一个NSSortDescriptor实例。
  • 上述NSSortDescriptor必须是传递给fetch request的数组中的第一个descriptor。

为性能而添加的缓存

在许多情况下,一个table view会表示同一种相近类型的数据。一个fetch request在table view controller创建的时候被定义,并且在应用的生命周期内不会改变。如果能给NSFetchedResultsController实例添加一个缓存,当应用重新启动而且数据没有改变的时候,这无疑是极好的,这样table view能迅速初始化。特别是对大型数据集,缓存特别有用。

OBJECTIVE-C

[self setFetchedResultsController:[[NSFetchedResultsController alloc] initWithFetchRequest:request managedObjectContext:moc sectionNameKeyPath:@"department.name" cacheName:@"rootCache"]];

SWIFT

fetchedResultsController = NSFetchedResultsController(fetchRequest: request, managedObjectContext: moc, sectionNameKeyPath: "department.name", cacheName: "rootCache")

就如上面展示的,cacheName参数在NSFetchedResultsController实例初始化的时候设置,fetched result controller会自动获得这个缓存。之后数据的加载几乎是瞬间的事。

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

推荐阅读更多精彩内容