我们知道 CoreData 里存储的是具有相同结构的一系列数据的集合,TableView 正好是用列表来展示一系列具有相同结构的数据集合的。所以,要是 CoreData 和 TableView 能结合起来,CoreData 查询出来的数据能同步地显示在 TableView 上,更好一点就是 CoreData 里的改动也能同步到 TableView 上,那就再好不过了。可喜的是,确实有这样一个 API,那就是 NSFetchedResultsController,相信不少人对这个东西都不陌生,因为用 Xcode 创建带有 CoreData 的 Master-Detail 模板工程时,就是用这个接口来实现的。这篇文章也主要是围绕着模板工程中的代码进行介绍,如果你对这块比较熟悉的话,不妨直接去看模板里的代码;如果你是第一次听说这个 API,不妨继续看下去,相信会对你有帮助的。
直接上代码:
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource, NSFetchedResultsControllerDelegate>
@property (strong, nonatomic) NSManagedObjectContext *context;
@property (strong, nonatomic) UITableView *tableView;
@property (strong, nonatomic) NSFetchedResultsController *fetchedResultController;
@end
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.title = @"Navigation";
UIBarButtonItem *leftItem = [[UIBarButtonItem alloc] initWithTitle:@"添加" style:UIBarButtonItemStyleDone target:self action:@selector(addStudent)];
self.navigationItem.leftBarButtonItem = leftItem;
//APPdelegate操作
AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
self.context = delegate.persistentContainer.viewContext;
[self setUpTableView];
}
- (void)setUpTableView {
self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.backgroundColor = [UIColor whiteColor];
self.tableView.tableFooterView = [UIView new];
[self.view addSubview:self.tableView];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
// sections 是一个 NSFetchedResultsSectionInfo 协议类型的数组,保存着所有 section 的信息
return self.fetchedResultController.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// sectionInfo 里的 numberOfObjects 属性表示对应 section 里的结果数量
id<NSFetchedResultsSectionInfo> sectionInfo = self.fetchedResultController.sections[section];
return sectionInfo.numberOfObjects;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:@"cell"];
}
// 通过这个方法可以直接获取到对应 indexPath 的实体类对象
Student *student = [self.fetchedResultController objectAtIndexPath:indexPath];
cell.textLabel.text = student.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"%d",student.age];
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 45;
}
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
return 0.00000001;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section {
return 0.00000001;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
return nil;
}
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section {
return nil;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
Student *student = [self.fetchedResultController objectAtIndexPath:indexPath];
[self.context deleteObject:student];
[self.context save:nil];
}
}
//NSFetchedResultsController 的初始化
- (NSFetchedResultsController <Student *>*)fetchedResultController {
//创建 fetchedResultsController 需要指定一个 fetchRequest,这很好理解,因为 fetchedResultsController 也需要查询 CoreData 数据库里的数据,需要注意的是,指定的这个 fetchRequest 必须要设置 sortDescriptors 也就是排序规则这个属性,不设置直接运行的话,程序是会直接崩溃的,这是因为 fetchedResultsController 需要根据这个排序规则来规定数据该以什么顺序显示到 tableView 上,而且这个 fetchRequest 指定之后就不可以再修改了
if (!_fetchedResultController) {
NSFetchRequest *fetchRequest = [Student fetchRequest];
fetchRequest.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"age" ascending:YES]];
//fetchLimit 之前讲过是指定获取数据的上限数量,而 fetchBatchSize 是分批查询的数据量大小
fetchRequest.fetchBatchSize = 10;
fetchRequest.fetchLimit = 100;
//context 就是上下文的对象;sectionNameKeyPath 可以指定一个 keypath 来为 tableView 生成不同的 section,指定成 nil 的话,就只生成一个 section; cacheName 用来指定一个缓存的名字,加载好的数据会缓存到这样一个私有的文件夹里,这样可以避免过多的从 CoreData 数据库里查询以及计算的操作
_fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.context sectionNameKeyPath:@"age" cacheName:@"StudentTable"];
_fetchedResultController.delegate = self;
//指定的泛型 Student 就是 fetchRequest 查询的数据类型。这些都配置之后调用 performFetch: 方法就可以执行查询操作了。返回的数据保存在 fetchedResultsController 的 fetchedObjects 属性里,不过我们一般不会直接用到它
[_fetchedResultController performFetch:nil];
}
return _fetchedResultController;
}
上一步里我们实现了把 fetchedResultsController 里的数据绑定到 TableView 上,但还没完成同步更新的实现,例如 CoreData 数据库里新插入了数据,TableView 这时也可以自动更新。实现这个功能,只需要实现 fetchedResultsController 的 delegate 就可以了。
NSFetchedResultsControllerDelegate 里有一个 NSFetchedResultsChangeType 枚举类型,其中的四个成员分别对应 CoreData 里的增删改查:
typedef NS_ENUM(NSUInteger, NSFetchedResultsChangeType) {
NSFetchedResultsChangeInsert = 1,
NSFetchedResultsChangeDelete = 2,
NSFetchedResultsChangeMove = 3,
NSFetchedResultsChangeUpdate = 4
}
5个代理方法:
// 对应 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:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeMove:
[self.tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath];
break;
case NSFetchedResultsChangeUpdate:
{
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
Student *stu = anObject;
cell.textLabel.text = stu.name;
cell.detailTextLabel.text = [NSString stringWithFormat:@"%d",stu.age];
break;
}
case NSFetchedResultsChangeDelete:
[self.tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
}
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
// 更新完后会回调这里,调用 tableView 的 endUpdates.
[self.tableView endUpdates];
}
- (void)addStudent {
NSString *name = [NSString stringWithFormat:@"student-%u", arc4random_uniform(9999)];
int16_t age = (int16_t)arc4random_uniform(10) + 10;
Student *student = [NSEntityDescription insertNewObjectForEntityForName:@"Student" inManagedObjectContext:self.context];
student.name = name;
student.age = age;
NSError *error;
[self.context save:&error];
}
over!!!