NSFetchedResultsController的简单封装 - UITableView与CoreData的完美结合

NSFetchedResultsController的简单封装 - UITableView与CoreData的完美结合

本文将简单分析NSFetchedResultsController这个控制器类的用法和开源控件DEFetchRC的使用方法。

如果您在阅读本文时,对CoreData还没有任何基础概念,这里给您推荐一套来自objcio.cn的CoreData系列教程,相信能对您有所帮助。

基本概念MVC与MVVC

从传统意义的MVC到MVVC的转变,相信大家都厌烦了术语套话,这些设计模式概念用自然语言描述显得十分抽象,我们今天用Cocoa的类来简单描述一下这两种模式。

传统MVC Model - View - Controller

刚入门Cocoa时,我们接触到的最基本的类,就是UIViewController和UIView类,他们对应的MVC模型中的Controller和View,Model(数据模型)则可以是CoreData的数据,网络请求的数据,或者其他一些格式的数据,MVC的运作模式就是Controller将Model内容填入View,View响应交互让Controller处理数据并且修改View。

MVVC Model - View - Controller - ViewModel

经过实战我们发现,Controller的代码量庞大不堪,一方面操作数据,一方面操作View,臃肿不堪,难以维护。所以MVVC模型应运而生,在MVC的基础上,加入了ViewModel概念,这个(些)ViewModel可以作为Controller的属性,剥离Controller中复杂的任务,例如:

  1. 替Controller实现TableView和CollectionView的回调。
  2. 剥离数据库操作
  3. 剥离网络请求

这样让Controller专注于处理View布局、交互和动画,把任务剥离,降低整体项目的耦合性。

NSFetchedResultsController

基于高度模块化和低耦合性的设计趋势,NSFetchedResultsController类应运而生。
你可能还不知道这个类可以完成什么工作,头文件中如此描述:

This class is intended to efficiently manage the results returned from a Core Data fetch request.
这个类用于高效地响应CoreData数据变化

本文的Demo代码见zsy78191/DEFetchRC
初始化代码,这段代码摘自苹果官方模版,我们自建DETableViewFetchRC类用于绑定,这里在DETableViewFetchRC类中实现NSFetchedResultsController相关方法,使用的参数包括

  1. self.entityName 实体名称
  2. self.managedContext CoreData的上下文
  3. self.predicate 检索谓语
  4. self.sortKey 排序键
  5. self.ascending 排序升序或降序
  6. _fetchController.delegate NSFetchedResultsControllerDelegate委托
@property (nonatomic, strong) NSFetchedResultsController* fetchController;

- (NSFetchedResultsController *)fetchController
{
    if (_fetchController != nil) {
        return _fetchController;
    }
    
    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:self.entityName inManagedObjectContext:self.managedContext];
    [fetchRequest setEntity:entity];
    
    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];
    [fetchRequest setPredicate:self.predicate];
    
    // Edit the sort key as appropriate.
    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:self.sortKey ascending:self.ascending];
    NSArray *sortDescriptors = @[sortDescriptor];
    
    [fetchRequest setSortDescriptors:sortDescriptors];
    
    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    _fetchController= [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedContext sectionNameKeyPath:nil cacheName:nil];
    _fetchController.delegate = self;
    
    NSError *error = nil;
    if (![_fetchController performFetch:&error]) {
        // Replace this implementation with code to handle the error appropriately.
        // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
        ////LOGMARK(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    
    return _fetchController;
}

绑定好的NSFetchedResultsController通过KVO观察CoreData上下文变化,并通知我们修改View,具体实现如下:

绑定tableView的DataSource

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [[self.fetchController sections] count];
}

注:NSFetchedResultsController是支持TableView的Section功能的,我们在这个例子中国年并没有使用,读者可以根据需要进行修改。

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchController sections][section];
    return [sectionInfo numberOfObjects];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    if([self checkDataSourceCanResponse:@selector(tableView:cellAtIndexPath:withData:)])
    {
        NSManagedObject *data = [self.fetchController objectAtIndexPath:indexPath];
         
        //在这里处理Cell内容
    }
    return nil;
}

绑定增删改

这里NSFetchedResultsController提供了NSFetchedResultsControllerDelegate协议,用于绑定Model和View,如下。

#pragma mark - fetch


- (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:UITableViewRowAnimationAutomatic];
            break;
            
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationLeft];
            break;
            
        default:
            return;
    }
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(NSManagedObject*)anObject
       atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath *)newIndexPath
{
    UITableView *tableView = self.tableView;
    switch(type) {
        case NSFetchedResultsChangeInsert:
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
            
        case NSFetchedResultsChangeDelete:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationLeft];
            break;
            
        case NSFetchedResultsChangeUpdate:
            [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
            
        case NSFetchedResultsChangeMove:
            [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            [tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    [self.tableView endUpdates];
}

至此,NSFetchedResultsController的基本使用方法都介绍了,接下来介绍一下我做的一个对NSFetchedResultsController的简单封装,zsy78191/DEFetchRC

DEFetchRC

一个NSFetchedResultsController的简单封装

DETableViewFetchRC将自动绑定CoreData的ManageContext和TableView,绑定好以后,数据的任何变动,包括增删改查,都会自动调用对应Cell的更新,不需要自己写任何代码做通知挂钩等。

Demo中使用Apple的CoreData模版,CoreData的Context在AppDelegate中。

- (NSManagedObjectContext*)managedObjectContext
{
    AppDelegate* delegate = [UIApplication sharedApplication].delegate;
    return delegate.managedObjectContext;
}

DETableViewFetchRC初始化

@property (nonatomic, strong) DETableViewFetchRC* tableFetchResultController;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self.managedObjectContext setUndoManager:[[NSUndoManager alloc] init]];
    
   
    self.tableFetchResultController = [DETableViewFetchRC fetchRCWithView:self.tableView
                                                                  reciver:self
                                                           managedContext:self.managedObjectContext
                                                               entityName:@"Item"
                                                                  sortKey:@"date"
                                                                ascending:YES
                                                                predicate:nil];
}

其中第一参数输入需要绑定(bind)的 TableView(如果使用DECollectionViewFetchRC则传入CollectionView),第二个参数是DETableViewFetchDelegate的委托,主要用来实现Cell内容填充回调:

- (UITableViewCell *)tableView:(UITableView *)tableView cellAtIndexPath:(NSIndexPath *)indexPath withData:(id)data
{
    Item* item = data;
    UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
    cell.textLabel.text = item.title;
    cell.detailTextLabel.text = [item.date description];
    return cell;
}

这里的CoreData实例名字是Item,有title和date两个属性,分别为NSString和NSDate。

这样就完成了一步绑定,CoreData的Context有变动时自动更新调用Cell更新回调,更新Cell,增删改都自动完成。效果如下:

最终效果

DEFetchRCDemo.gif

详细的代码请参考zsy78191/DEFetchRC

总结

本文作为怎样降低iOS代码耦合性一文的延伸阅读,介绍了NSFetchedResultsController类的基本使用方法,这个类作为CoreData的辅助控制器,在哲学高度强化了CoreData和TableView的绑定,原理上也适用于CoreData和CollectionView或其它第三方控件的绑定,灵活中不失稳重,值得细细研究一番。

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

推荐阅读更多精彩内容