IOS开发入门(13)-Core Data介绍

IOS开发入门(13)-Core Data介绍

前言

对于很多程序,外观对于其价值只是一个关键部分。没有数据,就没有什么可以显示的。而对于许多程序来说,数据必须持久话。在CarValet应用程序中,到目前为止数据都是临时的,重新运行,数据就会消失。我们需要一种方式来在应用程序多次运行之间保存数据

iOS提供的许多方法可以实现这一点:可以将对象原封不动保存到一个文件里,将对象及其关系翻译为XML并且存储到另一个文件中,甚至创建自己的SQLite数据库。无论选择哪儿种方式,都需要编写大量的子程序,包括保存一辆汽车、读取一辆汽车、查找一辆汽车、删除一辆汽车,以及更多。使用数据库,一些基本操作将被处理,但仍然有大量工作要做:我们需要定义SQL数据模型,设计并编写SQL查询,甚至更多。

所以这里我们开始介绍Core Data,他为我们做了大量的工作,方便了不少

介绍Core Data

Core Data是苹果公司用于将应用程序模型进行持久化的一项技术。它具有多层关系数据库的全部能力,还添加了对iOS和Mac OS的用户体验的集成环境。尽管Core Data威力强大,但开始利用它只需要做相对很少的工作。(本章只是简单的使用了一下Core Data,想真正利用Core Data还请自行查阅资料,如果我有资料的话会发在我的电子资源存放地,当然里面还有其他的东西,想要的话可以免费下载

在更加详细地介绍Core Data之前,理解如何在典型的应用程序中运用它是非常重要的。对于大多数应用程序,苹果公司推荐一种整体架构,名为Model-View-Controller(MVC)。

实现类被拆分为三个功能区域。在MVC中,模型完全是关于应用程序数据的:创建、变更、删除和修改。所有这些行为被封装到独立于用户界面的类中。做得好的话,任何与模型相关的操作都可以在命令行或图形视图中进行。外观取决于视图层。

视图层是应用程序的可视化用户体验。它包含所有有访问和修改数据相关的东西,以及任何其他的可视化应用程序元素,例如偏好设置。其关注点是外观和感觉。需要显示的信息来自于模型。视图层解决信息如何展示的问题。

控制器位于视图和模型之间。它解决从协调应用程序行为到控制动作流等各种事情,充当模型和视图的中间人。控制器通常是应用程序中最复杂的部分。除了位于模型和用户体验之间的控制器,还可能是其他控制器,用于与操作系统交互、处理通信,以及完成很多其他事情。如果应用程序针对打印或文本有不同的视图层,那么还会有针对那些特殊视图的其他控制器。

Core Data专注于MVC的模型层,还有一些部分可提供对用户界面控制器的支持。和在大多数数据库中一样,Core Data可以被分解为三个主要的层次:数据存放在哪里,数据的格式,以及数据访问环境。存储区是保存的数据被放置的地方,并且可能存在不止一个存储区。数据格式有managed object model(托管对象模型)定义。数据通过managed object context(托管对象上下文)来访问,并且同时可以有不止一个context处于活跃状态。

以下是相关的类:

  • NSpersistentStoreCoordinator协调用于存储数据的所有存储区。对于iOS,设备上通常只有一个存储区。
  • NSManagedObjectModel描述应用程序中使用的所有类型的数据对象。
  • NSManagedObjectContext是一组数据对象的管理器。这个上下文包含一些规则,用户存储区中找到的实际数据元素,一些已找到的数据元素以及机器的当前状态。多个上下文同时处于活跃状态是可能的,小童的对象在每个上下文中有不同的状态

将CarValet应用程序迁移到Core Data

理解Core Data的最好方法就是使用。

首先向应用程序中添加Core Data框架。执行以下步骤

  • 打开CarValet应用程序,在Xcode Navigator的顶部选择这个项目,打开CarValet应用程序的目标编辑区域,查找Editor区域(在当前这种情况下,是Project Editpr)的Target,并选择CarValet应用程序target(Targets列表中应当只有一个元素
    这里写图片描述
  • 保持这个目标被选中,确保General标签在Project Editor的Project Details区域的顶部显示。现在向下滚动Project Details区域,直到Linked Frameworks and Liaries,并且使用+按钮添加Core Data框架。
    这里写图片描述

添加CDCar模型

下一步,我们需要创建Core Data使用的文件以创建实体及其其他配置。同样的描述文件和编辑器可以用于从CarValet应用程序这样简单的单实体配置文件,到包含多个实体、实体属性、默认值以及关系非常复杂的模型的任何项目

添加汽车模型只需要完成以下几个步骤:

  • 使用Core Data分类的Data Mode模板向项目中添加一个新文件。将此文件称左CarValet并将其刚好添加到Car.h的上面
    这里写图片描述
  • 打开新的CarValet.xdatamodeld文件,如果他还没有显示的话。应当能看到与下图类似的编辑器
    这里写图片描述
  • 通过单机左下方的Add Entity按钮添加名为CDCar的实体
    这里写图片描述
  • 为Car对象已有的每个属性都添加一个特性(attribute),除了carInfo。可以在Assistant编辑器中打开Car.h文件,并确保得到所有属性。当添加每个属性时,按照当前模型设置器类型

类型不明显的属性只有year。可以将其定义为16位整数,因为年份在很长时间内不可能比32767还大,而不管选用的是哪种日历格式。当完成之后,这些属性如下图所示
这里写图片描述
原始Car数据对象可以在init中设置一些默认值。Core Data可让我们完成同样的事情,尽管在某些方面不太灵活。使用Data Model检查器将1908设置为年份的默认值。如下图所示
这里写图片描述
既然CDCar实体已经被定义,就可以创建NSManagedObject的一个子类来使用点表示法访问这些属性。唯一需要注意的地方是,属性默认为NSObject子类。这意味着任何整数、浮点数、布尔值等都是NSNumber。尽管即将执行的一步就能让我们使用标量,如NSInteger来表示原始类型,但还是有一些不想要的结果。在当前这种情况,Core Data将NSData转换为NSTimeInterval

保持Model编辑器仍打开,选择Editor|Create NSManagedObject Subclass,并且如果有提示的话,选择CDCar,然后将这个文件与其他所有源代码文件一起保存。
这里要注意的有两个地方:
这里写图片描述
这里写图片描述

添加Core Data样板代码

在能够使用任何数据之前,需要为应用程序初始化Core Data。需要为下图所示的所有三个类创建对象。

我们使用的是基于Xcode的Master-Detail Application模板。唯一修改就是添加对CoreData.h的导入,以及两个#define
这里写图片描述
步骤如下:(解释在代码中)

  • 修改AppDelegate.h文件:
#import <UIKit/UIKit.h>

#import <CoreData/CoreData.h>

#define MyModelURLFile          @"CarValet"//这是我们刚才创建的CoreData模型文件的名称
#define MySQLDataFileName       @"CarValet.sqlite"//这是由持久化存储区域协调器管理的存储区的名称,并且是个SQLite数据库


@interface AppDelegate : UIResponder <UIApplicationDelegate>

@property (strong, nonatomic) UIWindow *window;

@property (readonly, strong, nonatomic) NSManagedObjectContext
*managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel
*managedObjectModel;
@property (readonly, strong, nonatomic) NSPersistentStoreCoordinator
*persistentStoreCoordinator;


- (void)saveContext;
- (NSURL *)applicationDocumentsDirectory;


@end
  • 修改AppDelegate.m文件:
#import "AppDelegate.h"
#import "AboutViewController.h"

@interface AppDelegate ()

@end

@implementation AppDelegate
//3条@synthesize为.h文件中的只读属性提供读/写权限
@synthesize managedObjectContext = _managedObjectContext;
@synthesize managedObjectModel = _managedObjectModel;
@synthesize persistentStoreCoordinator = _persistentStoreCoordinator;

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    UIColor *Mocha = [UIColor colorWithDisplayP3Red:128.0/255 green:64.0/255.0 blue:0/255.0 alpha:1.0];//1

    [[UIButton appearance]setTitleColor:Mocha forState:UIControlStateNormal];//2
    [[UIBarButtonItem appearance] setTintColor:Mocha];

    UITabBarController *tabBarController = (UITabBarController*)self.window.rootViewController;//1
    //2
    AboutViewController *aboutViewController = [[AboutViewController alloc]initWithNibName:@"AboutViewController"
                                                                                    bundle:[NSBundle mainBundle]];
    UITabBarItem *aboutItem = [[UITabBarItem alloc]initWithTitle:@"About" //3
                                                           image:[UIImage imageNamed:@"tag"]
                                                             tag:0];
    [aboutViewController setTabBarItem:aboutItem];//4
    NSMutableArray *currentItems = [NSMutableArray arrayWithArray:tabBarController.viewControllers];//5
    [currentItems addObject:aboutViewController];//6
    [tabBarController setViewControllers:currentItems animated:NO];//7
    return YES;
}



- (void)applicationDidEnterBackground:(UIApplication *)application {
    // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
    // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}


- (void)applicationWillEnterForeground:(UIApplication *)application {
    // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}


- (void)applicationWillResignActive:(UIApplication *)application
{
    // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
    // Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
    [self saveContext];
}

- (void)applicationWillTerminate:(UIApplication *)application
{
    // Saves changes in the application's managed object context before the application terminates.
    [self saveContext];
}

- (void)saveContext
{
    NSError *error = nil;
    NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
    if (managedObjectContext != nil) {
        if ([managedObjectContext hasChanges] && ![managedObjectContext save:&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.
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

#pragma mark - Core Data stack

// Returns the managed object context for the application.
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
- (NSManagedObjectContext *)managedObjectContext
{
    if (_managedObjectContext != nil) {
        return _managedObjectContext;
    }

    NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
    if (coordinator != nil) {
        _managedObjectContext = [[NSManagedObjectContext alloc] init];
        [_managedObjectContext setPersistentStoreCoordinator:coordinator];
    }
    return _managedObjectContext;
}

// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
    if (_managedObjectModel != nil) {
        return _managedObjectModel;
    }
    NSURL *modelURL = [[NSBundle mainBundle] URLForResource:MyModelURLFile withExtension:@"momd"];
    _managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
    return _managedObjectModel;
}

// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
    if (_persistentStoreCoordinator != nil) {
        return _persistentStoreCoordinator;
    }

    NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:MySQLDataFileName];

    NSError *error = nil;
    _persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
    if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&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.

         Typical reasons for an error here include:
         * The persistent store is not accessible;
         * The schema for the persistent store is incompatible with current managed object model.
         Check the error message to determine what the actual problem was.


         If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.

         If you encounter schema incompatibility errors during development, you can reduce their frequency by:
         * Simply deleting the existing store:
         [[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]

         * Performing automatic lightweight migration by passing the following dictionary as the options parameter:
         @{NSMigratePersistentStoresAutomaticallyOption:@YES, NSInferMappingModelAutomaticallyOption:@YES}

         Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.

         */
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return _persistentStoreCoordinator;
}

#pragma mark - Application's Documents directory

// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory
{
    return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}


@end

当现在运行程序的时候,它看上去没有什么不同。这是因为我们所做的所有事情就是定义模型并且添加代码以准备使用Core Data,还没有任何用于创建和显示CDCar的代码

转换CarTableViewController

使用Core Data可以改变数据访问、创建、删除和更新的方式。要想转换应用程序的每个部分,我们需要识别这些区别并添加或修改相关代码。下表显示了这些区别以及需要对CarTableViewController所做的修改

修改为Core Data所做的修改需要修改的代码
1设置托管对象上下文在viewDidLoad中设置指向主要托管对象上下文的变量
2通过托管对象上下文访问数据对象设置数据获取请求,从托管对象上下文获得恰当的实体数据
3让Core Data处理汽车的添加和删除使用Core Data调用来添加和删除汽车。使用这些调用的结果更新arrayOfCars
4使用新的托管数据对象执行数据访问使用CDCar替换Car
5从原始类型改为对象将依赖interger和float数据类型的地方修改为使用NSNumber
1. 修改汽车表视图修改:添加托管对象上下文

第一处修改需要一个对托管对象上下文的引用。AppDelegate已经有了一个,因此汽车表视图控制器苏需要的只是一个引用。由于托管对象上下文在许多方法中使用,因此需要创建一个实例变量,尽管property并不是必须的。步骤如下:

  • 将AppDelegate.h和CDCar+CoreDataProperties.h导入到CarTableViewController.m中,并且删除Car.h的导入语句
  • 将如下声明添加到@implementation下面的花括号中的arrayOfCars的下边,创建一个实例变量:
NSManagedObjectContext *managedContextObject;
  • 将以下代码添加到viewDidLoad方法中,恰好在调用super的语句下面的位置,从而设置托管对象上下文:
AppDelegate *appDelegate = [[UIApplication shareApplication] delegate];

managedContextObject = appDelegatemanagedObjectContext;
2.汽车表视图修改:使用托管对象上下文访问数据

下一步,我们需要将数组的内容设置为当前的CDCar、对象。这需要做一系列修改。首先,从托管对象上下文获取数据,这需求在抓取请求中描述这些数据。可以是简单地指定一种特定实体类型的所有对象,也可以是非常复杂的过滤和排序

抓取请求被用做executeFetchRequest:error:的一部分,这是个用于获取数据对象数组的NSManagedObjectContext方法。该方法还可被用于当数据有变化时获得更新过的数据对象集合,变化包括添加或删除等。由于汽车数组在汽车表视图控制器的多个方法中被更新,因此执行以下步骤:

  • 将arrayOfCars的声明从NSMutableArray改为NSArray。与此同时,在managedObjectContext:的下边添加如下代码来声明抓取请求:
NSFetchRequest *fetchRequest;
  • 将viewDidLoad方法更新为下面代码,有加有删
-- (void)viewDidLoad {
    [super viewDidLoad];
    AppDelegate *appDelegate = [[UIApplication sharedApplication]delegate];

    managedContextObject = appDelegate.managedObjectContext;
    NSError *error=nil;
    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"CDCar"];//1 创建查找类CDCar的所有实体的抓取请求。其他方法和属性可用于添加过滤、排序,甚至批量加载对象
    arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest error:&error];//2将汽车数组设置为上下文中所有符合抓取请求标准的托管对象——在此处为所有汽车

    if (error != nil) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]); //3 读取对象时出错。将日志打印到文件对学习游泳。但更好的办法是如果个能的话,尽力从错误中恢复;如果无法恢复的话,将当前情况通知用户并告诉用户可以尝试的解决办法
        abort();//4对abourt()的调用来自系统提供的模板。它所做的所有事情就是创建崩溃日志并且终止程序
    }

    self.navigationItem.leftBarButtonItem = self.editButton;

//    arrayOfCars = [NSMutableArray new];
//    
//    [self newCar:nil];
//    self.navigationItem.leftBarButtonItem = self.editButton;

}
3.汽车表示图修改:使用Car Data添加和删除汽车

当前的汽车添加方式简单分配了一个新的Car对象并且将其插入到可变数组中。Car对象负责设置任何必须的初始值。删除仅仅是从数组中删除对Car对象的调用。使用可变数组假设内存从不耗尽,错误从不发生,并且回话之间的状态保存由别的代码实现。

使用Core Data带来了持久化、错误检查,以及更好的内存管理。此外还有一些额外的工作,但主要是错误检查。要想对添加汽车的代码做出修改,将newCar:方法代码替换为下面的代码:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    return [arrayOfCars count];
}

- (IBAction)newCar:(id)sender {
    NSLog(@"here");
    CDCar *newCar=[NSEntityDescription insertNewObjectForEntityForName:@"CDCar"
                                                inManagedObjectContext:managedContextObject];//1 在当前的托管对象上下文中创建新的汽车对象

    newCar.dateCreated = [NSDate date];//2初始化汽车的创建日期

    NSError *error;
    arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest
                                                      error:&error];//3重新生成当前的汽车数组,已包含新的汽车对象

    if(error != nil) {
        NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
        abort();
    }



//    Car *newCar = [Car new];
//    
//    [arrayOfCars insertObject:newCar atIndex:0];//1 将汽车插入到数组的前边
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];//2 创建一个NSindexPath对象来指定新单元格的位置——他的section和row

    [self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];//3 让表视图在新的索引路径上插入一个对象。这回事表视图调用数据源来查找第0表格段第0行的数据——也就是数组的第一个元素。因为数组已经被更新,所以新的单元格会被返回
//    //[self.tableView reloadData];
}

删除对象只需要对托管对象上下文的方法调用,那样就会将那个对象标记为已删除,尽管直到保存上下文时才会从存储区删除它

使用下面代码修改tableView:commitEditingSyle:forRowAtIndexPath:中的删除条件分支

- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // Delete the row from the data source
        [managedContextObject deleteObject:arrayOfCars[indexPath.row]];//1让托管对象上下文来删除CDCar对象
        NSError *error = nil;
        arrayOfCars = [managedContextObject executeFetchRequest:fetchRequest error:&error];

        if(error != nil) {
            NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
            abort();
        }
    } //else if (editingStyle == UITableViewCellEditingStyleInsert) {
        // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view
    //}
    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
4.汽车表视图修改:改为CDCar并跟新汽车单元格

我们所需要做的全部事情,即使切换到新的CDCar对象类。有三个视图控制器使用了Car对象:CarTableViewController、CarTableViewCell和ViewCarTableViewController

对于ViewCarTableViewController和CarTableViewCell:

  • 在.h文件中,在@class声明中和myCar属性中将Car修改为CDCar
  • 在.m文件中,将Car.h的导入语句修改为导入CDCar+CoreDataProperties.h

现在修改carToView:的返回值类型:

  • 在ViewCarProtocol.h中,将@class声明改为使用CDCar,并将返回值类型修改为CDCar
  • 在CarTableViewController.m中,将carToView的返回值类型修改为CDCar。这只需要修改方法的声明行:
- (CDCar*)carToView{
5.汽车表视图修改:更新为使用NSNumber

我们用的iOS版本是10.1,所以这里直接就能编译过了,而且运行也是正确的。self.myCar.year本身就是整数,不需要再额外的[self.myCar.year integerValue]转换了

现在可以运行程序了。我们一开始看起来不知道哪里发生变化了。可以尝试一下添加新的汽车,然后cmd+shift+H,返回主屏幕。然后重新运行一下程序,你会发现,原来的数据并没有随着程序的重启而消失,依然保存下来。到这里,说明我们现在已经成功的将CarValet应用程序迁移到了Core Data上了

更加简单的表视图:NSFetchedResultsController

到目前为止,在将CarValet应用程序转换为使用Core Data后,我们已经使用一个修改了的汽车数组版本来管理表视图。每一次数据有修改,一个新的数组就会产生。目前这之所以可行,是由于没有很多的数据(我们之前是每次更新数据,就创建一个新的数组,即arrayOfCars每次都新创建)。然而,当汽车数量变得更大时这样式不可行的。当有太多数据对象时,最好的情况是我们会遇到性能问题。更有可能会遇到内存问题

看起来应该写更少的代码。毕竟,如果托管对象上下文可以生成数据,那么它必须拥有关于总共多少启辰机器排列顺序的信息。此时,我们拥有了计算表格段数据以及表格段中数、特定索引路径中有哪儿些数据元素,以及基于数据的修改而更新表格的代码。

其实这些代码都不用写,因为系统提供了NSFetchResultsController及其关联的协议。它们的设计目的就是通过如下步骤使得概览基于Core Data的数据对象的表视图变得易于管理:

  • 配置表格中的段数与行数
  • 得到索引路径中的数据条目
  • 返回表格段头标题
  • 跟踪托管对象上下文中的修改,并且允许使用委托协议来执行表格更新
  • 批量获取数据,并且可选择将数据缓存到文件中来提升性能

第一部分:集成NSFetchedResultsController

我们需要一个实例变量来存放抓取结果控制器。将如下声明添加到CarTableViewController.m顶部@implementation语句后面的fetchRequest声明之下:

NSFetchedResultsController *fetchedResultsController;

现在需要初始化抓取结果控制器。这样做需要创建 正确的抓取请求。这是由于MSFetchedResultsController将应用程序托管对象上下文抓取请求的结果映射到了索引路径。

抓取结果控制器至少需要一个过滤器和一个排序。过滤器可以是nil,但是排序必须至少指定一个key。

抓取结果可以具有可选的批处理大小,尽管对于如此少量的数据没有必要。对于更大的数据集,批处理大小限制了每次读取多少数据元素,因此也限制了内存中的元素数量。

处理代码如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];

    managedContextObject = appDelegate.managedObjectContext;
    fetchRequest = [NSFetchRequest fetchRequestWithEntityName:@"CDCar"];

    NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
                                        initWithKey:@"dateCreated"
                                        ascending:NO];//1在创建时创建一条简单的排序规则,将最新的数据显示在顶部

    [fetchRequest setSortDescriptors:@[sortDescriptor]];//2将抓取请求的爱须描述符设置为这条新的排序规则。注意必须将排序描述符设置为描述符数组,即使只有一个。这就是这条语句使用@[]数组字面构造器的原因
    fetchedResultsController = [[NSFetchedResultsController alloc]//3从应用程序委托中,初始化使用刚刚分配的抓取请求结果控制器以及托管对象上下文。只有一个表格段,因此不需要表格段名称。也没有缓存
                                initWithFetchRequest:fetchRequest
                                managedObjectContext:managedContextObject
                                sectionNameKeyPath:nil
                                cacheName:nil];

    NSError *error=nil;
    [fetchedResultsController performFetch:&error];//4让控制器读取初始数据集,并且处理仍可能发生的错误

    if (error!=nil) {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
    self.navigationItem.leftBarButtonItem = self.editButton;
}
1更新基本表视图数据源方法

UITableViewDataSource实现了用于返回表格段数目、返回给定表格段中的表格行数,以及返回给定索引路径的单元格的三个核心方法。这些方法中的每一个现在都可以使用抓取结果控制器。执行以下步骤来更新这些方法。

  • numberOfSectionsInTableView:改为如下代码
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {

    //return 1;
    return [[fetchedResultsController sections]count];
}
  • tableView:numberOfRowsInSection:改为如下代码
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {

    //return [arrayOfCars count];
    id <NSFetchedResultsSectionInfo> sectionInfo;
    sectionInfo = [fetchedResultsController sections][section];
    return [sectionInfo numberOfObjects];
}
  • tableView:cellForRowAtIndexPath:中设置单元格中汽车对象的代码行修改如下:
 cell.myCar = [fetchedResultsController objectAtIndexPath:indexPath];
2更新、删除以及查看汽车

挡在模拟器中运行这个程序时,我们可以看到已经有汽车了。然而,当我们尝试添加或者删除汽车的时候,就会导致程序的崩溃。这是因为支持这些行为的方法仍然使用的是旧的基于数组的管理方法。所以我们需要更新一下代码。按照以下步骤来使用抓取控制器

  • tableView:commitEditingStyle:forRowAtIndexPath:方法中:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        [managedContextObject deleteObject:[fetchedResultsController objectAtIndexPath:indexPath]];
        NSError *error = nil;
        [fetchedResultsController performFetch:&error];
        if(error != nil) {
            NSLog(@"Unresolved error %@, %@",error,[error userInfo]);
            abort();
        }
    }
    [tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
  • newCar:中,将生成arrayOfCars的代码替换如下
[fetchedResultsController performFetch:&error];
  • carToView的返回值替换如下
return [fetchedResultsController objectAtIndexPath:currentViewCarPath];
  • 删除arrayOfCars实例变量的声明

现在程序能够正常运行了

第2部分:实现NSFetchedResultsControllerDelegate

代码能够正常运行,但是还有一个潜在的问题。每一次通过添加、删除或修改来更新托管对象上下文的数据时,都会想抓取结果控制器发送performFetch:消息。这个调用可能比仅仅更新修改了的数据需要更多的工作和时间。

抓取结果控制器有能力观察托管对象上下文中的修改,并且当这些变化发生时调用一些方法。我们只需要支持NSFetchedResultsControllerDelegate协议

  • 打开CarTableVIewController.h,将下列代码添加进去
#import <CoreData/CoreData.h>
  • 添加协议
<ViewCarProtocol,NSFetchedResultsControllerDelegate>
1.添加NSFetchedResultsControllerDelegate方法
  • 通过viewDidLoad中初始化fetchResultsController的代码行的后面添加如下代码,将当前的CarTableViewController实例设置为抓取结果控制器委托:
fetchedResultsController.delegate = self;
  • 在carToView的上面添加如下代码,这个#pragma被用于快速找到新的协议支持
#pragma mark - NSFetchedResultsControllerDelegate

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView beginUpdates];
}
  • 现在通知表视图更新已完成并应当更新,在上面的代码下方添加如下代码:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [self.tableView endUpdates];
}
  • 通过添加如下代码来添加基于修改类型完成更新表格段这一工作的协议方法。
- (void)controller:(NSFetchedResultsController *)controller
   didChangeObject:(id)anObject
       atIndexPath:(nullable NSIndexPath *)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(nullable NSIndexPath *)newIndexPath {

    UITableView *tableView = self.tableView;

    switch (type) {//1基于修改的类型,确定需要做哪一种更新
        case NSFetchedResultsChangeInsert://2档新对象插入之后,在表格中的恰当位置插入一个单元格
            [tableView insertRowsAtIndexPaths:@[newIndexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
        case NSFetchedResultsChangeDelete://3一个对象被删除,因此删除相应的单元格
            [tableView deleteRowsAtIndexPaths:@[indexPath]
                             withRowAnimation:UITableViewRowAnimationFade];
            break;
//        case NSFetchedResultsChangeUpdate://4这类修改在对象被修改或更新时发生。在此处,我们需要做刷新表示数据的单元格所需要做的一切事情
//            code to update the content of the cell at indexPath
//            break;
//        case NSFetchedResultsChangeMove://5最后一处修改是将数据单元格从表格中的一个地方一道另一个地方。通常这意味着删除旧的单元格并插入一个新的
//            [tableView deleteRowsAtIndexPaths:@[indexPath]
//                             withRowAnimation:UITableViewRowAnimationFade];
//            [tableView insertRowsAtIndexPaths:@[newIndexPath]
//                             withRowAnimation:UITableViewRowAnimationFade];
//            break;

        default:
            break;
    }
}
2.启用委托方法调用

如果现在运行CarValet应用程序,委托方法并没有被调用。可以通过添加NSLog语句或者在controllerWillChangeContent中设置断电,然后添加或删除汽车来看到这一点。表格会被更新,但并不是由于委托方法。

观察者仅在托管对象上下文被保存时才会别通知。保存发生的唯一地方就是在应用程序委托中,当应用程序进入后台或退出时。这些事件会调用自定义方法saveContext,它会向所有修改了的托管对象上下文发送save消息。

要触发委托方法,需要在添加或删除汽车时保存上下文。tableViewcommitEditingStyle:forRowAtIndexPath:删除一辆汽车,而newCar添加一辆。在这些方法中将[fetchedResultsController performFetch:&error];替换成[managedContextObject save:&error];

save:消息会触发委托消息流:初始的controllerWillChangeContent消息,所需数量的Controller:diddChangeObjectPath:forChangeType:newIndexPath:消息,然后是controllerDidChangeContent:消息。

现在运行程序,添加数据或删除数据时崩溃跳出,在调试器输出中发现错误原因如下:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
----来自谷歌翻译----
由于未被捕获的异常'NSInternalInconsistencyException'终止应用程序,原因:'无效更新:第0节中的行数无效。更新(8)后现有部分中包含的行数必须等于该列中包含的行数 更新前的部分(8),加或减从该部分插入或删除的行数(1插入,0删除),加或减移动到该部分的行数(0移动,0移动 出)。

这个异常告诉我们,表视图期望的行数与实际的行数不匹配,者之所以说得通,是由于原始的代码和新的委托方法都更新了表格。有两处代码都对桶以对象执行了添加和删除操作,要修改这个错误,步骤如下:

  • 在tableViews:commitEditingStyle:forRowAtIndexPath:中删除对deleteRowsAtIndexPaths:withRowAnimation:的调动,就是下面那段代码
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationFade];
  • 从newCar:中删除最后两行:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
[self.tableView insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];

运行程序,ok,完美运行咯。

今天的介绍就到这里咯

我的另一个博客站点:Arnold-你们好啊

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值