原文链接:http://www.objc.io/issue-4/full-core-data-application.html

前言

先戳这里获取示例工程。

如何构建

首先创建固化栈对象,提供CD数据模型和文件名,返回一个管理对象上下文。然后我们将会创建CD数据模型。接下来,我们将会创建简单的表格视图通过抓取结果控制器来展示根列表上的单件,然后一步一步添加交互,通过添加单件,导航到子单件详情,删除单件以及添加撤销的操作。

创建栈

我们将要在主线程队列上创建管理对象上下文。在老代码中,你可能会看到[[NSManagedObjectContext alloc] init]。现在,你可以调用initWithConcurrencyType:进行初始化来显式的声明栈使用并发数据模型:

- (void)setupManagedObjectContext
{
    self.managedObjectContext = 
         [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    self.managedObjectContext.persistentStoreCoordinator = 
        [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:self.managedObjectModel];
    NSError* error;
    [self.managedObjectContext.persistentStoreCoordinator 
         addPersistentStoreWithType:NSSQLiteStoreType
                      configuration:nil
                                URL:self.storeURL 
                            options:nil 
                              error:&error];
    if (error) {
        NSLog(@"error: %@", error);
    }
    self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];
}

核对错误十分重要,因为不这样在开发过程中很可能会导致失败。当你改变了你的数据模型,CD探测到这个变化后将不会继续执行。你还可以传递一些选项来告诉CD碰到这些情况该怎么做。注意到最后一行添加了一个undo manager,我们在后续需要它。在iOS,你需要显式的添加它,而在OSX平台这个是默认的。

上述的代码创建了一个十分简易的CD栈:一个管理对象上下文,有这一个PSC指向一个固化仓库。更复杂的创建是可以的;而一样的地方在于在不同独立的线程中拥有多个管理对象上下文。

创建数据模型

创建数据模型是容易的,正如我们添加一个新的文件到我们工程一样,选择数据类型模板。这个模型文件将会生成.momd的后缀,它将会在运行时被我们创建的NSManagedObjectModel所加载,是我们固化仓库所必要的文件。数据模型的源是简单的XML文件,并且在我们的经验来看,你在审核它是否属于控制的时候一般不会遇到任何整合问题。同样也可以在代码中创建一个管理对象模型,如果你愿意这么做的话。

一旦你创建了数据模型,你可以添加带两个属性的单件实体:标题(字符串),还有顺序(整型)。然后你可以添加两个关系:父亲,相关单件的父亲;还有孩子,相关单件的孩子,这是一个一对多的关系(一个父亲多个孩子)。设置关系为另外一个的倒置,那么意味着你设置a的父亲为b,那么b将会有一个a作为它得孩子。

一般来说,你甚至可以使用排序的关系,并且完全不用去管order属性。但是,当你使用它们的时候并不能很好与抓取结果控制器协作。我们必须要实现部分的抓取结果控制器或者实现重排序,我们选择后者。

现在,从菜单选择Editor > Create(编辑 > 创建)NSManagedObject子类...然后根据绑定一个实体创建NSManagedObject的子类。这样会生成两个文件Item.h and Item.m。这里的.h文件会有额外的类别,我们等下会删掉。

创建仓库类

对我们数据模型,我们需要创建一个根结点来作为为我们单件树得起始。我们需要一个地方来创建这个根结点一遍后续找到它。因此,就是完成这个目的我们创建了一个简单得仓库类。它来管理对象上下文,还有一个方法rootItem。在我们的应用代理,我们可以在启动的时候找到这个根结点然后将它传给我们根视图控制器。作为优化,你可以用UD(userdefault)保存这个对象的ID来达到快速检索的效果。

- (Item*)rootItem
{
    NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:@"Item"];
    request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", nil];
    NSArray* objects = [self.managedObjectContext executeFetchRequest:request error:NULL];
    Item* rootItem = [objects lastObject];
    if (rootItem == nil) {
        rootItem = [Item insertItemWithTitle:nil 
                                      parent:nil 
                      inManagedObjectContext:self.managedObjectContext];
    }
    return rootItem;
}

添加单件大多数总是很直接的。但是,我们需要设置更大的order属性相对于已经存在单件的父母结点。我们将设置第一个孩子结点的order为0,并且每一个后续的孩子结点的order值1或者更高。我们在Item类创建自定义的方法放入逻辑:

+ (instancetype)insertItemWithTitle:(NSString*)title
                             parent:(Item*)parent
             inManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
    NSUInteger order = parent.numberOfChildren;
    Item* item = [NSEntityDescription insertNewObjectForEntityForName:self.entityName
                                               inManagedObjectContext:managedObjectContext];
    item.title = title;
    item.parent = parent;
    item.order = @(order);
    return item;
}

孩子结点的数量是一个简单不过的方法:

- (NSUInteger)numberOfChildren
{
    return self.children.count;
}

为了支持自动更新我们的表格视图,我们将使用抓取结果控制器。抓取结果控制器是一个对象可以满足管理抓取一个大数量单件的请求并且是在使用表格视图时候CD的最佳拍档,我们会在下一节看到这个特性:

- (NSFetchedResultsController*)childrenFetchedResultsController
{
    NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:[self.class entityName]];
    request.predicate = [NSPredicate predicateWithFormat:@"parent = %@", self];
    request.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"order" ascending:YES]];
    return [[NSFetchedResultsController alloc] initWithFetchRequest:request 
                                               managedObjectContext:self.managedObjectContext 
                                                 sectionNameKeyPath:nil 
                                                          cacheName:nil];
}

添加支持表格视图的抓取结果控制器(这个名字太恶了。。。)

在我们之前说的轻量级视图控制器中,我们揭示了如何分离把我们表格视图中的数据源分离开了。我们将会用抓取结果控制器做相同的事情;我们创建一个独立的类FetchedResultsControllerDataSource来扮演视图的数据源,并且根据抓取结果控制器自动更新表格视图。

我们进行表格视图初始化,如下:

- (id)initWithTableView:(UITableView*)tableView
{
    self = [super init];
    if (self) {
        self.tableView = tableView;
        self.tableView.dataSource = self;
    }
    return self;
}

当我们设置抓取结果控制器的时候,我们需要设置自身的代理然后执行最初抓取。很容易忘记performFetch:调用,然后你将没有获取任何结果(也不报错):

- (void)setFetchedResultsController:(NSFetchedResultsController*)fetchedResultsController
{
    _fetchedResultsController = fetchedResultsController;
    fetchedResultsController.delegate = self;
    [fetchedResultsController performFetch:NULL];
}

因为我们的类遵循UITableViewDataSource协议,我们需要执行一些必要的协议方法。在这两个协议方法中我们都请求抓取请求控制器获取必须的信息:

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

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

但是,当我们需要创建cells的时候,需要一些简单的步骤:我们让抓取结果控制器获取正确的对象,我们将表格视图的cell进行入队,然后告诉我们的代理去根据对象配置我们的cell。现在,我们有一个很棒解耦,作为视图控制器来说只要关注根据数据模型对象来更新cell。

- (UITableViewCell*)tableView:(UITableView*)tableView 
        cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
    id object = [self.fetchedResultsController objectAtIndexPath:indexPath];
    id cell = [tableView dequeueReusableCellWithIdentifier:self.reuseIdentifier
                                             forIndexPath:indexPath];
    [self.delegate configureCell:cell withObject:object];
    return cell;
}

创建表格视图控制器

现在我们创建了一个视图控制器来展示我们的单件列表同时使用了我们创建的类。
在栗子应用中,我们创建了故事板,并添加了内置表格试图控制器的导航栏控制器。这自动设置了试图控制器得数据源,这并不是我们想要的。因此,我们在viewDidLoad,我们需要进行如下操作:

fetchedResultsControllerDataSource =
    [[FetchedResultsControllerDataSource alloc] initWithTableView:self.tableView];
self.fetchedResultsControllerDataSource.fetchedResultsController = 
    self.parent.childrenFetchedResultsController;
fetchedResultsControllerDataSource.delegate = self;
fetchedResultsControllerDataSource.reuseIdentifier = @"Cell";

在抓取结果控制器数据源的初始化器,试图控制器的数据源被赋值。ID重用与故事板中的相匹配。现在,我们需要实现代理方法:

- (void)configureCell:(id)theCell withObject:(id)object
{
    UITableViewCell* `cell` = theCell;
    Item* item = object;
    cell.textLabel.text = item.title;
}

当然,你要懂得是你做的不仅仅可以对text label进行设置。现在我们基本各就各位来展示我们的数据了,但是这里还没有将这些数据加载上,它看上去什么都没有。

添加交互

我们要添加两个方法与数据进行交互。首先,我们要先能添加数据。然后我们要实现让抓取结果控制区的代理方法去刷新表格视图,并且支持删除跟撤销。

添加Items

为了添加单件,我们从Clear这个APP偷来交互方式(作者对其评价很高)。我们添加一个text field做为表格式图的头,然后修改表格视图的内容内置偏移来确保其默认隐藏。这里有插入单件的相关代码,在textFieldShouldReturn:我们实现如下:

[Item insertItemWithTitle:title 
                   parent:self.parent
   inManagedObjectContext:self.parent.managedObjectContext];
textField.text = @"";
[textField resignFirstResponder];

监听变化

下一步是确认你的表格式图插入一行新建的单件。有许多种方式可以实现这点,但是我们使用抓取结果控制器的代理方法:

- (void)controller:(NSFetchedResultsController*)controller
   didChangeObject:(id)anObject
       atIndexPath:(NSIndexPath*)indexPath
     forChangeType:(NSFetchedResultsChangeType)type
      newIndexPath:(NSIndexPath*)newIndexPath
{
    if (type == NSFetchedResultsChangeInsert) {
        [self.tableView insertRowsAtIndexPaths:@[newIndexPath]
                              withRowAnimation:UITableViewRowAnimationAutomatic];
    }
}

抓取结果控制器同时可以调用此类方法完成删除,修改还有移动操作。如果你在同一时间遇到多种变化,你可以实现两个方法这样可以让表格视图可以在同一事件完成所有得动画。对于单一单件插入和删除来说,它没有任何区别,但是你如果你在某一时间选择了同步更新,它看起来更优雅点:

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

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

使用集合类视图

值得了解的是抓取结果控制器并不单单局限于表格视图;当你遇到任何视图的时候可以使用它。因为他们是基于index-path的缘故,它们同样很好得使用于集合类视图,尽管你不幸的需要完成几个类似的环节来让它更完美的工作,做一个集合类视图不需要完成beginUpdates and endUpdates方法,但是需要实现performBatchUpdates。处理这个东西的时候,你可以收集所有得待更新项,然后在controllerDidChangeContent执行内部的块。

实现你自定义的抓取结果控制器

你不用死板使用NSFetchedResultsController。事实上,在很多场景中创建类似的类去完成你APP中特定的功能是有意义得。你可以做的是订阅NSManagedObjectContextObjectsDidChangeNotification通知,然后在获取通知后的userInfo字典里将含有改变对象的列表,插入对象,删除对象。然后你想怎么处理怎么处理。

传递数据模型对象

下你在我们可以添加和展示单件了,是时候确定我们可以做一个子列表。在故事板里,我们可以从cell拖拽一根线(segue)连上一个新建的场景。给这个segue命名是一个很明智的做法,这样在同一个试图控制器的segue多的时候不至于混淆。

我处理segues的方式像这样:首先,你试着定义你此时要操作的segue,并且你为每一个segue拉出来对应一个独立的方式来表示目的试图控制器。

- (void)prepareForSegue:(UIStoryboardSegue*)segue sender:(id)sender
{
    [super prepareForSegue:segue sender:sender];
    if ([segue.identifier isEqualToString:selectItemSegue]) {
        [self presentSubItemViewController:segue.destinationViewController];
    }
}

- (void)presentSubItemViewController:(ItemViewController*)subItemViewController
{
    Item* item = [self.fetchedResultsControllerDataSource selectedItem];
    subItemViewController.parent = item;
}

子视图控制器唯一需要就是单件。对于单件来说,可以从管理对象上下文中获取。我们从数据源获取选中的单件。就是这么简单。

将管理对象上下文设置为APP代理的一个属性是一个常见的不幸的模式,并且我们哪里都可以获取到这个东西。这是个很烂的主意。如果你甚至希望为你试图控制器的部分层级使用不同的管理对象上下文,这样将十分难于重构,并且你的代码更难于测试。

现在,试着添加单件到子列表上,你有可能得到华丽的崩溃。这是因为我们现在拥有两个抓取结果控制器,一个服务于顶层的视图控制器,同时也有一个服务于为根视图控制器。后一个试着更新它所述的表格视图(不在当前屏幕上),然后就崩溃了。结局方法是告诉我们数据源停止监听抓取结果控制器代理方法:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    self.fetchedResultsControllerDataSource.paused = NO;
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    self.fetchedResultsControllerDataSource.paused = YES;
}

一个实现的内部方式是设置抓取结果控制器的代理为nil,这样将不会收到任何更新。我们需要做的是当离开pause状态的时候重新添加它。

- (void)setPaused:(BOOL)paused
{
    _paused = paused;
    if (paused) {
        self.fetchedResultsController.delegate = nil;
    } else {
        self.fetchedResultsController.delegate = self;
        [self.fetchedResultsController performFetch:NULL];
        [self.tableView reloadData];
    }
}

performFetch方法将会确定你的数据源激将要更新。当然,更优雅的实现绝对不是设置代理为nil,而是在pause的时候保留变化列表,相应地更新表格式图当你跳出pause状态。

删除

为了支持删除,我们需要执行一些步骤。首先,我们需要设置表格式图支持删除,然后我们需要从CD中删除对象并确保我们的数据顺序始终保证正确。

为了支持滑动删除,我们需要在数据源中实现下面两个方法:

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

  - (void)tableView:(UITableView *)tableView 
 commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
  forRowAtIndexPath:(NSIndexPath *)indexPath {
     if (editingStyle == UITableViewCellEditingStyleDelete) {
         id object = [self.fetchedResultsController objectAtIndexPath:indexPath]
         [self.delegate deleteObject:object];
     }
 }

不是马上地执行删除,而是我们告诉代理(视图控制器)去干掉这个对象。用这种方式,我们不用去将我们仓库里的对象与我们的数据源共享(数据源需要在整个工程中重用),并且我们能一直保持响应各种动作时候的灵活性。视图控制器只是简单地在管理对象上下文中调用了deleteObject:方法。

然后,这里有两个问题需要被解决:当单件的孩子被删除的时候我们需要做什么,还有我们怎么保证顺序不变?幸运的是,繁殖删除比较简单,我们可以选择级联作为孩子关系的删除规则。

为了确保顺序的不变,我们要重写prepareForDeletion方法,然后把删除单件所有的兄弟单件更新为更高的order

- (void)prepareForDeletion
{
    NSSet* siblings = self.parent.children;
    NSPredicate* predicate = [NSPredicate predicateWithFormat:@"order > %@", self.order];
    NSSet* siblingsAfterSelf = [siblings filteredSetUsingPredicate:predicate];
    [siblingsAfterSelf enumerateObjectsUsingBlock:^(Item* sibling, BOOL* stop)
    {
        sibling.order = @(sibling.order.integerValue - 1);
    }];
}

现在我们差不多搞定了,我们可以与表格视图cell进行交互并且可以山删掉数据模型对象。最后一步要实现删除的必须代码是干掉数据模型对象时候视图进行相应的删除。在我们的数据源控制器提供的controller:didChangeObject:方法中我们添加如下代码:

...
else if (type == NSFetchedResultsChangeDelete) {
    [self.tableView deleteRowsAtIndexPaths:@[indexPath]
                          withRowAnimation:UITableViewRowAnimationAutomatic];
}

撤销操作支持

CD略屌的一点是集成了撤销操作。我们将增加震动撤销的效果。首先我们要告诉APP我们可以进行震动操做响应:

application.applicationSupportsShakeToEdit = YES;

现在,一旦震动触发时间,应用会让响应者找到撤销管理者(undo manager),并且执行撤销。在我们的视图控制器中,我们重写了以下UIResponder的两个方法:

- (BOOL)canBecomeFirstResponder {
    return YES;
}

- (NSUndoManager*)undoManager
{
    return self.managedObjectContext.undoManager;
}

现在,当摇晃手势响应的时候,管理对象上下文的撤销管理者将会收到一个撤销消息,然后撤销上次的变化。记住,在iOS上,一个管理对象上下文不需要默认拥有一个撤销管理者,所以我们需要在固化栈对其进行创建:

self.managedObjectContext.undoManager = [[NSUndoManager alloc] init];

现在差不多大功告成了。当你摇晃的时候,你将收到iOS带两个按钮的默认告警:一个是撤销,另外一个是取消。CD一个略屌的方面是它自动给你进行分组撤销。比如你添加动作是一个撤销变化,同样删除也算一个。

为了让撤销对用户来说更加容易,我们可以将我们的撤销操作命名,并且改写textFieldShouldReturn:第一行代码来实现功效:

NSString* title = textField.text;
NSString* actionName = [NSString stringWithFormat:
    NSLocalizedString(@"add item \"%@\"", @"Undo action name of add item"), title];
[self.undoManager setActionName:actionName];
[self.store addItem:title parent:nil];

现在,当用户摇晃手机的时候,他/她将获得更多得撤销信息,知道自己做的是什么撤销操作。

编辑

重排序

保存

保存的时机很重要。看情况,一般是applicationWillTerminate:执行一次或者根据实际也可能在applicationDidEnterBackground:执行,甚至在运行时执行。。(尼玛说了等于白说。。。。囧)

总结

作者自责写这个文章的时候选择了nil作为根单件。测试产生了一些问题,此处各种省略。。。略到底 OVER。。。


Cruise_Chan
729 声望71 粉丝

技能树点歪了...咋办