NSFetchedResultsController与 UITableView 的天作之和

本人有若干成套学习视频, 可试看! 可试看! 可试看, 重要的事情说三遍 包含Java, 数据结构与算法, iOS, 安卓, python, flutter等等, 如有需要, 联系微信tsaievan.

NSFetchedResultsController与 UITableView可以说是天设一对,地造一双

有了NSFetchedResultsController,我们可以将NSFetchedResultsController中的数据作为 tableView 的数据源

今天,我们用NSFetchedResultsController与 UITableView做了一个通讯录,效果如下:
Demo的大致效果

完成了大致的基本功能, 还有比如说,限制电话号码的格式之类的没有做,但这不是今天的重点

我们可以看出,在这个小 demo 中,只有三个控制器
  • contactListVC (联系人列表控制器)
  • contactAddVC(新增联系人控制器)
  • contactEditVC(编辑联系人控制器)

其中,只有contactListVC (联系人列表控制器)是有 tableView的,所以我们的FetchedResultsController主要是配合这个 controller 来将数据显示在 tableView 上

ContactListVC (联系人列表控制器)的内容

  • 要想使用NSFetchedResultsController,我们可以将NSFetchedResultsController作为 Controller的一个属性,利用懒加载对其进行初始化
初始化的方法中,有三个参数是必须的,
  1. 查询请求
  1. 管理对象上下文
  2. 分组依据

这三个参数告诉了FetchedResultsController这个控制器,从哪里,以什么样的方式去获取数据

#pragma mark *** 属性懒加载 ***
- (NSFetchedResultsController *)fetchedResultsController
{
    if (!_fetchedResultsController) {
        /**
         fetchResultController 的初始化方法
         * 参数1 :fetchRequest 查询请求
         * 参数2 :管理对象上下文
         * 参数3 :分组依据
         * 参数4 :缓存名
         */
        
        /* 先初始化一个查询请求 */
        NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Contact"];
        
        /* 给查询请求设置排序器,参数是排序依据,如果不设置,程序会崩溃 */
        request.sortDescriptors = @[ [NSSortDescriptor sortDescriptorWithKey:@"namePinYin" ascending:YES] ];
        
        _fetchedResultsController = [[NSFetchedResultsController alloc]initWithFetchRequest:request managedObjectContext:kYFCoreDataManager.managedObjectContext sectionNameKeyPath:@"sectionName" cacheName:nil];
        
        /* 设置 fetchedResultsController 的代理 */
        _fetchedResultsController.delegate = self;
        
        /* 执行查询 */
        [_fetchedResultsController performFetch:nil];
        /* 执行查询之后刷新 tableView */
        [self.tableView reloadData];
    }
    return _fetchedResultsController;
}
  • 其次,我们设定视图控制器为FetchedResultsController的代理
    其中FetchedResultsController的代理方法是

-(void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath

只要是 controller 中的数据元素发生变化,就一定会走这个方法,

  • 改变的类型主要有四种方式:
    • 增加数据
    • 删除数据
  • 移动数据
  • 改变数据
在这个方法中,对 tableView进行适时的刷新,无论是新增数据,还是删除数据,都能立即更新到 tableView 上
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath
{
    /* 在这个方法中,你需要判断新增加的数据是组还是行 */
    /* 先获取到当前tableView 的组数,以及数据库中的组数 */
    NSInteger tableViewSections = self.tableView.numberOfSections;
    NSInteger fetchSections = self.fetchedResultsController.sections.count;
    
    /* switch 语句来判断 fetchController 中的 object 改变类型 */
    
    /*
     * 一共有四种类型
     * NSFetchedResultsChangeInsert  插入数据
     * NSFetchedResultsChangeDelete  删除数据
     * NSFetchedResultsChangeMove    移动数据
     * NSFetchedResultsChangeUpdate  更新数据
     */
    switch (type) {
            /* 新增的本质就是 tableView 新加数据 */
        case NSFetchedResultsChangeInsert:
            
            
            [self.tableView beginUpdates];
            /* 如果 tableViewSections 和 fetchSections 的数量不一致,则表明增加的是组*/
            
            if (tableViewSections != fetchSections) {
                
                /* 注意,是在新的 newIndexPath中插入组 */
                [self.tableView insertSections:[NSIndexSet  indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationTop];
            }
            /* else 表示相等,表明新增的是组 */
            else
            {
                /* 也是在 newIndexPath 中插入行 */
                [self.tableView insertRowsAtIndexPaths:@[ newIndexPath ] withRowAnimation:UITableViewRowAnimationTop];
            }
            
            
            [self.tableView endUpdates];
            break;
            /* 删除的本质就是 tableView 删除数据 */
        case NSFetchedResultsChangeDelete:
            
            
            
            [self.tableView beginUpdates];
            /* 判断逻辑和新增数据一样,如果组数不想等,则表明新增的是组 */
            
            if (tableViewSections != fetchSections) {
                [self.tableView deleteSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationTop];
                
            }
            /* 如果组数相等,则新增的是行 */
            else
            {
                [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationTop];
            }
            
            [self.tableView endUpdates];
            
            break;
            /* 移动(如果数据的排序发生变化,则走移动) */
        case NSFetchedResultsChangeMove:
            /* 直接刷新数据 */
            [self.tableView reloadData];
            break;
            /* 更新(如果数据的排序未发生变化,只是其他发生变化,则走更新) */
        case NSFetchedResultsChangeUpdate:
            [self.tableView beginUpdates];
            
            /* 只刷新改变的那一行代码,因此是旧的 indexPath */
            [self.tableView reloadRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationTop];
            [self.tableView endUpdates];
            break;
            
        default:
            break;
    }
    
}

此时, tableView的数据源方法就可以直接从fetchedResultsController拿数据就好了
#pragma mark *** Table view data source数据源方法 ***

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    /* 在你使用了 fetchResultsController 之后,返回的组的数量就是从 fetchResultsController 中来获取 */
    return self.fetchedResultsController.sections.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    /* self.fetchedResultsController.sections的返回值是NSArray<id<NSFetchedResultsSectionInfo>,那么用 section 去取数组中的元素就可以得到这个数组中的元素  */
    
    /* 这表明这个 id 类型的对象是遵循NSFetchedResultsSectionInfo协议的 */
    id <NSFetchedResultsSectionInfo> infos = self.fetchedResultsController.sections[section];
    
    /* 因而这个 id 类型的对象有 numberOfObjects 这个方法 */
    return [infos numberOfObjects];
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContactListVCCell" forIndexPath:indexPath];
    
    Contact *contact = [self.fetchedResultsController objectAtIndexPath:indexPath];
    
    cell.textLabel.text = contact.name;
    cell.detailTextLabel.text = contact.phoneNum;
    
    return cell;
}

ContactAddVC(新增联系人控制器)的代码内容

主要的代码就是利用 CoreData 框架来新增数据,具体步骤就再复习一遍

  1. 先用实体描述器初始化对象
  1. 给对象的属性赋值,值是从 textField 中来的
  2. 将对象保存在数据库中
    从数据库中获取数据的工作就不用这个控制器来做了,而是交给了fetchResultsController来做,只要数据库的对象发生了变化,就会走fetchResultsController的代理,来实现刷新 tableView 的功能

代码如下:

#pragma mark *** 处理按钮的点击事件 ***
// -------- 点击右边的保存按钮触发的事件 --------
- (IBAction)rightBarButtonItemClickAction:(UIBarButtonItem *)sender {
    
    // -------- 数据保存(数据插入的三个步骤) --------
    
    /* 1. 实体描述器初始化模型对象 */
    
    Contact *contact = [NSEntityDescription insertNewObjectForEntityForName:@"Contact" inManagedObjectContext:kYFCoreDataManager.managedObjectContext];
    
    /* 2. 给模型对象的属性赋值 */
    contact.name = self.nameTextField.text;
    contact.phoneNum = self.phoneNumTextField.text;
    contact.namePinYin = [CommonTool getPinYinFromString:contact.name];
    // 先取拼音,再取首字母,再大写
    contact.sectionName = [[contact.namePinYin substringToIndex:1] uppercaseString];
    
    [kYFCoreDataManager save];
    
    [self.navigationController popViewControllerAnimated:YES];
}
ContactEditVC(编辑联系人控制器)的代码内容

和ContactAddVC的实现逻辑基本相同,修改数据库的操作也只有三步:

  1. 拿到数据模型对象
    在ContactListVC 中,跳转界面的时候把 contac模型对象传给目标控制器
#pragma mark *** 跳转界面执行的方法 ***
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    if ([segue.identifier isEqualToString:@"ContactEditVC"]) {
        
        ContactEditVC *contactEditVC = (ContactEditVC *)[segue destinationViewController];
        contactEditVC.contact = [self.fetchedResultsController objectAtIndexPath:self.tableView.indexPathForSelectedRow];
    }
}

2.直接对模型对象属性进行修改
3.保存

#pragma mark *** 视图生命周期 ***
- (void)viewDidLoad {
    [super viewDidLoad];
    self.nameTextField.text = self.contact.name;
    self.phoneNumTextField.text = self.contact.phoneNum;
}

#pragma mark *** 点击 nacvigationBarButtonItem 触发的对象 ***
- (IBAction)rightBarButtonItemClickAction:(UIBarButtonItem *)sender {
    
        // -------- 相当于在数据库中修改数据 --------
      /* 直接对模型对象的属性进行修改 */
    self.contact.name = self.nameTextField.text;
    self.contact.phoneNum = self.phoneNumTextField.text;
    self.contact.sectionName = [[self.contact.namePinYin substringToIndex:1] uppercaseString];
    self.contact.namePinYin = [CommonTool getPinYinFromString:self.contact.name];
    
     /* 修改结束后进行保存 */
    [kYFCoreDataManager save];
    
     /* 然后 pop 掉当前控制器,返回到上一层控制器 */
    [self.navigationController popViewControllerAnimated:YES];
}
以上就是这个小 demo 的全部内容,感谢大家的支持!

PS. 本人有若干成套学习视频, 包含Java, 数据结构与算法, iOS, 安卓, python, flutter等等, 如有需要, 联系微信tsaievan.

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

推荐阅读更多精彩内容