CoreData 从入门到精通(五)CoreData 和 TableView 结合

我们知道 CoreData 里存储的是具有相同结构的一系列数据的集合,TableView 正好是用列表来展示一系列具有相同结构的数据集合的。所以,要是 CoreData 和 TableView 能结合起来,CoreData 查询出来的数据能同步地显示在 TableView 上,更好一点就是 CoreData 里的改动也能同步到 TableView 上,那就再好不过了。可喜的是,确实有这样一个 API,那就是 NSFetchedResultsController,相信不少人对这个东西都不陌生,因为用 Xcode 创建带有 CoreData 的 Master-Detail 模板工程时,就是用这个接口来实现的。这篇文章也主要是围绕着模板工程中的代码进行介绍,如果你对这块比较熟悉的话,不妨直接去看模板里的代码;如果你是第一次听说这个 API,不妨继续看下去,相信会对你有帮助的。

创建一个简单的 TableView 布局

在使用 CoreData 之前,首先我们来创建一个简单的 TableView 布局,对大多数人来说,这应该没什么难度,所以下面就直接贴代码,不会对代码进行解释了。
这里我们用 Storyboard 创建一个 TableViewController,首先配置 tableView 的 dataSource


- (void)viewDidLoad {
    [super viewDidLoad];
    // 添加编辑和插入按钮
    self.navigationItem.leftBarButtonItem = self.editButtonItem;
    self.navigationItem.rightBarButtonItem = [self addBarButtonItem];
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    
    return 100;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
    
    [self configureCell:cell atIndexPath:indexPath];
    return cell;
}

- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // deletions
    }
}

这是一个非常简单的 TableView,现在只能显示一些随机生成的数据。接下来我们来实现 NSFetchedResultsController 来和 CoreData 中的数据对接,CoreData 相关的代码,就直接用之前文章里创建的 Student 实体。如果有不了解的朋友,可以先去看一下这篇文章 CoreData 从入门到精通 (一) 数据模型 + CoreData 栈的创建

初始化 NSFetchedResultsController

先来看一下 NSFetchedResultsController 的初始化代码:

#pragma mark - NSFetchedResultsController

- (NSFetchedResultsController<Student *> *)fetchedResultsController {
    if (!_fetchedResultsController) {
        NSFetchRequest *fetchRequest = [Student fetchRequest];
        fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"studentId" ascending:YES]];
        fetchRequest.fetchBatchSize = 50;
        fetchRequest.fetchLimit = 200;
    
        NSFetchedResultsController *fetchController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.context sectionNameKeyPath:@"studentAge" cacheName:@"StudentTable"];
        
        fetchController.delegate = self;
        NSError *error;
        [fetchController performFetch:&error];
        
        [[[Logger alloc] init] dealWithError:error whenFail:@"fetch failed" whenSuccess:@"fetch success"];
        
        _fetchedResultsController = fetchController;
    }
    return _fetchedResultsController;
}

创建 fetchedResultsController 需要指定一个 fetchRequest,这很好理解,因为 fetchedResultsController 也需要查询 CoreData 数据库里的数据,需要注意的是,指定的这个 fetchRequest 必须要设置 sortDescriptors 也就是排序规则这个属性,不设置直接运行的话,程序是会直接崩溃的,这是因为 fetchedResultsController 需要根据这个排序规则来规定数据该以什么顺序显示到 tableView 上,而且这个 fetchRequest 指定之后就不可以再修改了;

fetchRequest-w600

context 就是上下文的对象;sectionNameKeyPath 可以指定一个 keypath 来为 tableView 生成不同的 section,指定成 nil 的话,就只生成一个 section;

sectionNameKeyPath-w600

cacheName 用来指定一个缓存的名字,加载好的数据会缓存到这样一个私有的文件夹里,这样可以避免过多的从 CoreData 数据库里查询以及计算的操作。

cacheName-w600

除此之外,fetchLimitfetchBatchSize 这两个属性也需要注意一下,fetchLimit 之前讲过是指定获取数据的上限数量,而 fetchBatchSize 是分批查询的数据量大小。因为一次性查询出过多的数据会消耗不少的内存,所以这里推荐给这两个属性设置一个合理的值;指定的泛型 Student 就是 fetchRequest 查询的数据类型。这些都配置之后调用 performFetch: 方法就可以执行查询操作了。返回的数据保存在 fetchedResultsControllerfetchedObjects 属性里,不过我们一般不会直接用到它。

fetchedResultsController 绑定到 TableView

下面来修改 tableView 的 dataSource 的方法将查询出来的数据集合和 TableView 绑定。

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
      // sections 是一个 NSFetchedResultsSectionInfo 协议类型的数组,保存着所有 section 的信息
    return self.fetchedResultsController.sections.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    // sectionInfo 里的 numberOfObjects 属性表示对应 section 里的结果数量
    id<NSFetchedResultsSectionInfo> sectionInfo = self.fetchedResultsController.sections[section];
    return sectionInfo.numberOfObjects;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
    // 通过这个方法可以直接获取到对应 indexPath 的实体类对象
    Student *student = [self.fetchedResultsController objectAtIndexPath:indexPath];
    [self configureCell:cell withStudent:student];
    return cell;
}
// 修改后的configureCell 方法
- (void)configureCell:(UITableViewCell *)cell withStudent:(Student *)student {
    cell.textLabel.text = [NSString stringWithFormat:@"%d:%@ age:%d", student.studentId, student.studentName, student.studentAge];
}

实现增删改查的同步更新

上一步里我们实现了把 fetchedResultsController 里的数据绑定到 TableView 上,但还没完成同步更新的实现,例如 CoreData 数据库里新插入了数据,TableView 这时也可以自动更新。实现这个功能,只需要实现 fetchedResultsControllerdelegate 就可以了。

NSFetchedResultsControllerDelegate 里有一个 NSFetchedResultsChangeType 枚举类型,其中的四个成员分别对应 CoreData 里的增删改查:

typedef NS_ENUM(NSUInteger, NSFetchedResultsChangeType) {
    NSFetchedResultsChangeInsert = 1,
    NSFetchedResultsChangeDelete = 2,
    NSFetchedResultsChangeMove = 3,
    NSFetchedResultsChangeUpdate = 4
}

delegate 里共有五个协议方法:

// 对应 indexPath 的数据发生变化时会回调这个方法
@optional
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath;

 // section 发生变化时会回调这个方法
@optional
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;

 // 数据内容将要发生变化时会回调
@optional
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;

 // 数据内容发生变化之后会回调
@optional
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;

 // 返回对应 section 的标题
@optional
- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName;

@end

想要实现 tableView 的数据同步更新可以按下面的代码来实现这几个 delegate 方法:

#pragma mark - NSFetchedResultsControllerDelegate
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    // 在这里调用 beginUpdates 通知 tableView 开始更新,注意要和 endUpdates 联用
    [self.tableView beginUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
    // beginUpdates 之后,这个方法会调用,根据不同类型,来对tableView进行操作,注意什么时候该用 indexPath,什么时候用 newIndexPath.
    switch (type) {
        case NSFetchedResultsChangeInsert:
            [self.tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
        case NSFetchedResultsChangeMove:
            [self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
            break;
        case NSFetchedResultsChangeUpdate:
            [self configureCell:[self.tableView cellForRowAtIndexPath:indexPath] withStudent:anObject];
            break;
        case NSFetchedResultsChangeDelete:
            [self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
            break;
    }
    
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    // 更新完后会回调这里,调用 tableView 的 endUpdates.
    [self.tableView endUpdates];
}

最后来实现一开始添加的编辑和插入按钮的操作:

- (UIBarButtonItem *)addBarButtonItem {
    UIBarButtonItem *addBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addRandomStudent)];
    
    return addBarButtonItem;
}
// 为了看到插入效果,可以把 fetchedResultsController 的fetchLimit 和 fetchBatchSize 调小一些.
- (void)addRandomStudent {
    NSString *name = [NSString stringWithFormat:@"student-%u", arc4random_uniform(100000)];
    int16_t age = (int16_t)arc4random_uniform(10) + 10;
    int16_t stuId = (int16_t)arc4random_uniform(INT16_MAX);
    Student *student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.context];
    student.studentName = name;
    student.studentAge = age;
    student.studentId = stuId;
    [self.context save:nil];
}

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
    if (editingStyle == UITableViewCellEditingStyleDelete) {
        
        Student *student = [self.fetchedResultsController objectAtIndexPath:indexPath];
        [self.context deleteObject:student];
        [self.context save:nil];
    }
}

到此为止 NSFetchedResultsController 的使用就讲完了,有了它 CoreData 和 TableView的结合是不是很方便呢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,118评论 29 470
  • 你可以没有脑子 但至少吃一堑长一智 你可以不漂亮 但至少身边要有几个朋友 你可以圈子不大 但至少圈子请干净 你可以...
    奶味萝莉阅读 140评论 0 0
  • 春天的一生痛苦 他一生幸福
    fup阅读 151评论 0 0