鍍金池/ 教程/ iOS/ 一個(gè)完整的 Core Data 應(yīng)用
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

一個(gè)完整的 Core Data 應(yīng)用

在這篇文章中,我們將建立一個(gè)小型但卻全面支持 Core Data 的應(yīng)用。此應(yīng)用允許你創(chuàng)建嵌套的列表;每個(gè)列表的 item 都可以有子列表,這將允許你創(chuàng)建非常深層次的 items。為了讓大家完整的了解發(fā)生了什么,我們將通過使用手動(dòng)創(chuàng)建堆棧的方式來代替 Xcode 中 Core Data 的模板。這個(gè)應(yīng)用的代碼放到了 GitHub 上。

我們將怎么建立?

首先,我們創(chuàng)建一個(gè) PersistentStack 對(duì)象,為其提供一個(gè) Core Data 模型和一個(gè)文件名,PersistentStack 會(huì)返回一個(gè) managed object context。然后,我們將要?jiǎng)?chuàng)建我們的 Core Data 模型。接著,我們將創(chuàng)建一個(gè)簡(jiǎn)單的 table view controller 來顯示使用 fetched results controller 取回的 item 根目錄,并且通過增加 items,sub-items 的導(dǎo)航,刪除 items,增加 undo 支持,來一步一步進(jìn)行交互。

設(shè)置堆棧

我們將為主隊(duì)列創(chuàng)建一個(gè) managed object context。在比較老的代碼中,你可能見到 [[NSManagedObjectContext alloc] init]。而目前,你應(yīng)該用 initWithConcurrencyType: 初始化,以明確你是使用基于隊(duì)列的并發(fā)模型。

- (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];
}

檢查錯(cuò)誤是非常重要的,因?yàn)樵陂_發(fā)過程中,這很有可能經(jīng)常出錯(cuò)。當(dāng) Core Data 發(fā)現(xiàn)你改變了數(shù)據(jù)模型時(shí),就會(huì)暫停操作。你也可以通過設(shè)置選項(xiàng)來告訴 Core Data 在遇到這種情況后怎么做,這在 Martin 關(guān)于 遷移 的文章中徹底的解釋了。注意,最后一行增加了一個(gè) undo manager;我們將在稍后用到。在 iOS 中,你需要明確的去增加一個(gè) undo manager,但是在 Mac 中,undo manager 是默認(rèn)有的。

這段代碼建立了一個(gè)真正簡(jiǎn)單的 Core Data 堆棧:一個(gè)擁有持久化存儲(chǔ)協(xié)調(diào)器的 managed object context,其擁有一個(gè)持久化存儲(chǔ)。更復(fù)雜的設(shè)置都是可能的;最常見的是擁有多個(gè) managed object context(每一個(gè)都在單獨(dú)的隊(duì)列中)。

創(chuàng)建一個(gè)模型

創(chuàng)建模型比較簡(jiǎn)單,我們只需要增加一個(gè)新文件到我們的項(xiàng)目,在 Core Data 選項(xiàng)中選擇 Data Model template。這個(gè)模型文件將會(huì)被編譯成后綴名為 .momd 類型的文件,我們將會(huì)在運(yùn)行時(shí)加載這個(gè)文件來為持久化存儲(chǔ)創(chuàng)建需要用的 NSManagedObjectModel,模型的源碼是簡(jiǎn)單的 XML,根據(jù)我們的經(jīng)驗(yàn),一般來說當(dāng)你 check 到代碼版本管理中時(shí),應(yīng)該不會(huì)有任何 merge 的困難。如果你愿意,你還可以在代碼中創(chuàng)建一個(gè) managed object model。

一旦你創(chuàng)建了模型,你就可以增加 Item 實(shí)體,這個(gè)實(shí)體有兩個(gè)屬性:字符串類型的 title 和 integer 類型的 order。然后,增加兩個(gè)關(guān)系:一個(gè)叫做 parent,表示這個(gè) item 的父 item;另一個(gè)叫 children,是一個(gè)一對(duì)多的關(guān)系。設(shè)置它們?yōu)楸舜讼喾吹年P(guān)系,也就是說,你設(shè)置 a 的 parent 為 b,那么 b 就會(huì)自動(dòng)有一個(gè) children 為 a。

通常,你甚至可以完全拋開 order 屬性,而去使用排序好的的關(guān)系。然而,它們并不能很好的和 fetched results controllers(后面會(huì)用到)集成在一起工作。我們要么需要重新實(shí)現(xiàn) fetched results controller 的一部分,要么重新實(shí)現(xiàn)排序,通常我們都會(huì)選擇后者。

現(xiàn)在,從菜單中選擇 Editor > NSManagedObject subclass...,創(chuàng)建一個(gè)綁定到實(shí)體的 NSManagedObject 的子類,這將會(huì)創(chuàng)建兩個(gè)文件:Item.hItem.m。在頭文件中,會(huì)有一個(gè)額外的類別,我們需要將其刪除(這是遺留原因?qū)е碌模?/p>

創(chuàng)建一個(gè) Store 類

對(duì)于我們的模型,我們將創(chuàng)建一個(gè)根節(jié)點(diǎn)作為我們 item 樹的開始。我們需要一個(gè)地方來創(chuàng)建這個(gè)根節(jié)點(diǎn),并且方便以后找到。因此,我們可以通過創(chuàng)建一個(gè)簡(jiǎn)單的存儲(chǔ)類來達(dá)到這個(gè)目的。存儲(chǔ)類有一個(gè) managed object context,還有一個(gè) rootItem 方法。在 app delegate 中,我們將會(huì)在程序啟動(dòng)時(shí)查找這個(gè) root item,并且傳給了 root view controller。作為一種優(yōu)化,為了查找這個(gè) item 變得更快,你可以將 item 對(duì)象的 id 存儲(chǔ)到 user defaults 中:

- (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;
}

大多數(shù)情況下,增加一個(gè) item 都是簡(jiǎn)單的。然而,我們需要設(shè)置 order 屬性值比任何其父節(jié)點(diǎn)的子節(jié)點(diǎn)的值更大。我們將會(huì)設(shè)置第一個(gè)子節(jié)點(diǎn)的 order 值 為0,隨后每一個(gè)子節(jié)點(diǎn)都會(huì)增加1。我們?cè)?Item 類中創(chuàng)建一個(gè)自定義的方法來實(shí)現(xiàn):

+ (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;
}

獲得子節(jié)點(diǎn)數(shù)量的方法很簡(jiǎn)單:

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

為了支持自動(dòng)更新我們的 table view,我們需要使用 fetched results controller。Fetched results controller 是一個(gè)可以管理取出大量 item 請(qǐng)求的對(duì)象,同時(shí)對(duì)使用 Core Data 的 table view 來說,它也是一個(gè)完美的小伙伴,在下一節(jié)中我們將會(huì)用到:

- (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];
}

增加一個(gè)支持 Table-View 的 Fetched Results Controller

我們下一步是創(chuàng)建一個(gè) root view controller:一個(gè)從 NSFetchedResultsController 讀取數(shù)據(jù)的 table view。Fetched results controller 管理你的讀取請(qǐng)求,如果你為它分配一個(gè) delegate,那么在 managed object context 中發(fā)生的任何改變都會(huì)通知你。實(shí)際上,這意味著如果你實(shí)現(xiàn)了 delegate 方法,當(dāng)數(shù)據(jù)模型中發(fā)生相關(guān)變化時(shí),你可以自動(dòng)更新你的 table view。比如,你在后臺(tái)線程同步,并且把變化存儲(chǔ)到數(shù)據(jù)庫中,那么你的 table view 將會(huì)自動(dòng)更新。

創(chuàng)建 Table View 的 Data Source

更輕量的 View Controllers 這篇文章中,我們演示了怎么從 table view 中分離出 data source。這里,我們將會(huì)用同樣的方法創(chuàng)建一個(gè) fetched results controller。我們創(chuàng)建一個(gè)分離出的 FetchedResultsControllerDataSource 類,它扮演了 table view 的 data source,通過監(jiān)聽 fetched results controller,自動(dòng)更新 table view。

我們初始化一個(gè) table view 對(duì)象,初始化方法如下:

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

當(dāng)我們?cè)O(shè)置 fetch results controller 時(shí),我們需要設(shè)置自己為 delegate,并且執(zhí)行初始化的 fetch 操作。performFetch: 方法經(jīng)常容易被忘了調(diào)用,那么你將得不到結(jié)果(并且不會(huì)出錯(cuò)):

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

因?yàn)槲覀兊念悓?shí)現(xiàn)了 UITableViewDataSource 協(xié)議,我們需要實(shí)現(xiàn)相關(guān)的方法。在這兩個(gè)方法中,我們只需要向 fetched results controller 請(qǐng)求需要的信息:

- (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;
}

然而,當(dāng)我們需要?jiǎng)?chuàng)建 cell 的時(shí)候,只需要一些簡(jiǎn)單的步驟:向 fetched results controller 請(qǐng)求正確的對(duì)象,從 table view 出列一個(gè)cell,然后告訴 delegate (即一個(gè) view controller) 用相應(yīng)的對(duì)象配置這個(gè) cell。作為 view controller,只會(huì)關(guān)心用模型對(duì)象更新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;
}

創(chuàng)建 Table View Controller

現(xiàn)在,我們可以創(chuàng)建一個(gè) view controller,使用剛剛創(chuàng)建的類顯示 item 列表。在示例程序中,我們創(chuàng)建一個(gè) Storyboard,并且增加一個(gè)擁有 table view controller 的 navigation controller。這會(huì)自動(dòng)設(shè)置 view controller 作為數(shù)據(jù)源,而這不是我們想要的效果。因此,在我們的viewDidLoad中,我們做下面的操作:

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

在初始化 fetched results controller data source 時(shí),table view 的數(shù)據(jù)源可以被設(shè)置。reuse 標(biāo)識(shí)符匹配在 Storyboard 中相對(duì)應(yīng)的對(duì)象?,F(xiàn)在,我們需要實(shí)現(xiàn) delegate 方法:

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

當(dāng)然,除了設(shè)置 text 的 label 外,你還可以做更多的事情,但是你應(yīng)該已經(jīng)明白了要領(lǐng)?,F(xiàn)在我們已經(jīng)為顯示數(shù)據(jù)準(zhǔn)備好了相當(dāng)多的事情,但是卻仍然沒有增加數(shù)據(jù)的方法,這看起來非常空。

增加互動(dòng)

我們將會(huì)增加兩種和數(shù)據(jù)交互的方法。首先,我們需要實(shí)現(xiàn)增加 items。然后我們需要實(shí)現(xiàn) fetched results controller 的 delegate 方法去更新 table view,并且增加刪除和 undo 支持。

增加 Items

為了增加 items,我們借鑒 Clear 的交互設(shè)計(jì),這是我認(rèn)為最漂亮的應(yīng)用之一。我們?cè)黾右粋€(gè) text field 作為 table view 的頭,并修改 table view 的 content inset,確保它默認(rèn)保持隱藏,正如 Joe 在 scroll view 這篇文章中解釋一樣。像往常一樣,所有的代碼都在 github 上,這里是插入 item 相關(guān)的代碼,在 textFieldShouldReturn:

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

監(jiān)聽改變

下一步是確保 table view 會(huì)為新創(chuàng)建的 item 插入一行。有好幾種方法可以做到,但是我們將會(huì)使用 fetched results controller 的代理方法:

- (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];
    }
}

fetched results controller 也會(huì)在刪除、改變和移動(dòng)時(shí)調(diào)用一些方法(我們將在稍后實(shí)現(xiàn))。如果你一次有很多改變,你可以多實(shí)現(xiàn)兩個(gè)方法,那么 table view 將會(huì)動(dòng)畫地展現(xiàn)所有的改變。對(duì)于單個(gè) item 的插入和刪除,這并不會(huì)有任何不同,但是如果你選擇實(shí)現(xiàn)同時(shí)同步,那么將會(huì)變得更漂亮:

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

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

使用 Collection View

值得注意的是,fetched results controllers 并非只能用于 table views;你可以將它只用在任何 view 中。因?yàn)樗鼈兪腔?indexPath 的,所以它們能與 collection views 很好的一起工作。由于 collection view 沒有 beginUpdatesendUpdates 方法,卻有一個(gè) performBatchUpdates 方法,所以我們需要稍加改變。你可以收集你得到的所有更新,然后在 controllerDidChangeContent 中,用 block 執(zhí)行所有的更新。Ash Furrow 寫了一個(gè)關(guān)于如何做的例子。

實(shí)現(xiàn)你自己的 Fetched Results Controller

你不必使用 NSFetchedResultsController。實(shí)際上,在很多情況下,為你的程序創(chuàng)建一個(gè)類似的類將顯得更有意義。你可以做的是注冊(cè) NSManagedObjectContextObjectsDidChangeNotification。然后你就可以得到一個(gè) notification,userInfo 字典將會(huì)包含改變對(duì)象,插入對(duì)象,刪除對(duì)象的列表,然后你可以按你喜歡的方式執(zhí)行這些操作。

傳遞 Model 對(duì)象

現(xiàn)在我們可以增加并且列出 itmes 了,現(xiàn)在我們需要確定能夠創(chuàng)建 sub-lists。在 Storyboard 中,你可以通過拖拽一個(gè) cell 到 view controller 中來創(chuàng)建一個(gè) segue。最好給 segue 指定一個(gè)名字,這樣,如果一個(gè) view controller 中有多個(gè) segues 的話,我們就可以將其區(qū)分開了。

我處理 segues 的模式看起來像這樣:首先,你嘗試識(shí)別出這個(gè) segue,對(duì)于每一個(gè) segue,你為它的目標(biāo) view controller 單獨(dú)寫一個(gè)方法:

- (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;
}

子 view controller 需要唯一的東西就是item。通過 item,也可以得到 managed object context。我們從 data source 中得到選中的 item(通過 table view 選中的 item 的 index 值,從 fetched results controller 中取出正確的 item),就這么簡(jiǎn)單。

很不幸的是,在 app delegate 中,將 managed object context 作為一個(gè)屬性,然后總是在任何地方訪問它,這是模式非常常見。這其實(shí)是一個(gè)壞主意。如果你想要為你 view controller 中的一部分使用一個(gè)不同的 managed object context時(shí),將很難重構(gòu),此外,你的代碼將變得很難測(cè)試。

現(xiàn)在,嘗試在 sub-list 中增加一個(gè) item,你很有可能得到一個(gè) crash。這是因?yàn)槲覀儸F(xiàn)在有兩個(gè) fetched results controllers,一個(gè)是 topmost view controller,還有一個(gè)是root view controller。后者嘗試去更新它的 table view,而它的table view是離屏的(offscreen),就這樣所有的操作都crash了。解決方案是告訴我們的data source停止監(jiān)聽fetched results controller的代理方法:

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

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

一種方法就是在 data source 中設(shè)置 fetched results controller 的代理為 nil,這樣就再也不會(huì)收到更新通知了。當(dāng)我們離開 paused 狀態(tài)時(shí),還需要加上去:

- (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 就會(huì)確保你的 data source 保持最新的。當(dāng)然,更好的實(shí)現(xiàn)方法并不是設(shè)置代理為 nil,而是記錄每一個(gè)在 paused 狀態(tài)下的改變,相應(yīng)的,在離開 paused 狀態(tài)后,更新 table view。

刪除

為了支持刪除,我們需要花費(fèi)幾步操作。首先,我們需要確信我們的 table view 支持刪除。第二,我們需要從 core data 中刪除對(duì)象,并且保證我們的排序是正確的。

為了支持滑動(dòng)刪除,我們需要在 data source 中實(shí)現(xiàn)兩個(gè)方法:

- (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];
     }
}

我們需要通知代理(the view controller)刪除對(duì)象,而不是直接刪除。這樣,我們不需要將 store object 分配給data source(data source 在整個(gè)項(xiàng)目中都必須可重用),并且保持自定義操作的靈活性。view controller 只需在 managed object context 中簡(jiǎn)單的調(diào)用 deleteObject:。

然而,還有兩個(gè)重要的問題需要被解決:我們?cè)趺刺幚肀粍h除 item 的子 item,怎么強(qiáng)制我們的 order 變化?幸運(yùn)的是,傳播刪除是很簡(jiǎn)單的:在我們的數(shù)據(jù)模型中,我們可以選擇 Cascade 作為子關(guān)系的刪除規(guī)則。

為了強(qiáng)制我們的 order 變化,我們可以重寫 prepareForDeletion 方法,用更高一級(jí)的 order 更新所有兄弟節(jié)點(diǎn)。

- (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);
    }];
}

現(xiàn)在我們幾乎快完成了。我們可以與 table view 的 cell 交互,并且可以刪除模型對(duì)象。最后一步是實(shí)現(xiàn)一旦模型對(duì)象被刪除后,刪除 table view cell 的必要的代碼。在 data sources 的方法 controller:didChangeObject:... 中,我們?cè)黾恿硪粋€(gè) if 語句:

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

增加 Undo 支持

Core Data 優(yōu)點(diǎn)之一就是集成了 undo 支持。我們將為增加晃動(dòng)撤銷功能,第一步就是告訴程序我們可以這么做:

application.applicationSupportsShakeToEdit = YES;

現(xiàn)在,這個(gè)功能可以被任何抖動(dòng)觸發(fā),程序?qū)?huì)向 first responder 請(qǐng)求 undo manager,并且執(zhí)行一次 undo 操作。在上個(gè)月的文章中,我們了解了,一個(gè) view controller 也在響應(yīng)鏈中(responder chain),這也正是我們將要使用的。在我們的 view controller 中,我們重寫來自 UIResponder 類中的兩個(gè)方法:

- (BOOL)canBecomeFirstResponder {
    return YES;
}

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

現(xiàn)在,當(dāng)一個(gè)抖動(dòng)發(fā)生時(shí),managed object context 的 undo manager 將會(huì)得到一個(gè)undo消息,并且撤銷最后一次改變。記住,在 iOS 中,managed object context 默認(rèn)并沒有一個(gè) undo manager,(而在 Mac 中,新建的 managed object context 默認(rèn)是有的),所以我們需要在持久化堆棧中設(shè)置:

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

基本上就是這樣了?,F(xiàn)在,當(dāng)你抖動(dòng)時(shí),你將得到 iOS 默認(rèn)有兩個(gè)按鈕的提醒框:一個(gè)是 undo 按鈕,一個(gè) cancel 按鈕。Core Data 的一個(gè)非常好的特性是將改變自動(dòng)分組。比如,addItem:parent 將會(huì)記錄作為一個(gè) undo 處理。關(guān)于刪除,也是一樣的。

為了讓用戶管理 undo 操作更容易一些,我們可以給操作命名,并且將 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];

現(xiàn)在,當(dāng)用戶抖動(dòng)時(shí),除了普通的 "Undo" 標(biāo)簽外,他將得到更多的上下文環(huán)境。

編輯

編輯目前在示例程序中并不支持,但是這只是一個(gè)改變對(duì)象屬性的問題。比如,改變一個(gè) item 的 title,只需要設(shè)置 title 屬性就好了。改變 foo item 的 parent,只需要設(shè)置 parent 屬性為一個(gè)新值 bar,所有的東西都將得到更新,bar 現(xiàn)在有一個(gè) childrenfoo,因?yàn)槲覀兪褂?fetched results controllers,用戶界面同樣也會(huì)自動(dòng)更新。

重新排序

重新排序 cell,在現(xiàn)有程序中也是不可行的,但是這實(shí)現(xiàn)起來很簡(jiǎn)單。但是,還有一個(gè)需要注意的地方:如果你允許用戶重新排序,你將需要在 model 中更新 order 屬性,并且從 fetched results controller 得到一個(gè) delegate call(你需要忽略這個(gè)調(diào)用,因?yàn)閏ell已經(jīng)被移動(dòng)了)。這在 NSFetchedResultsControllerDelegate 的文檔中有解釋。

保存

保存非常簡(jiǎn)單,就是在 managed object context 中調(diào)用 save 而已。因?yàn)槲覀儾⒉恢苯釉L問 managed object context,所以是在 store 中進(jìn)行保存。唯一的困難的是什么時(shí)候去保存。Apple 的示例代碼在 applicationWillTerminate: 中執(zhí)行這個(gè)操作,但是這取決于你使用情況,這也有可能在 applicationDidEnterBackground: 中,甚至當(dāng)你程序運(yùn)行時(shí)調(diào)用。

討論

在寫這篇文章和示例程序時(shí),我初始時(shí)就犯了一個(gè)錯(cuò)誤:我沒有選擇使用一個(gè)空的根 item 來作為所有用戶創(chuàng)建的 item 的 parent,而是讓它們都指向了一個(gè) nil。這將造成很多問題:因?yàn)?view controller 中的父 item 可能是 nil,我們需要將 store(或 managed object context) 傳給每一個(gè)子 view controller。同樣的,強(qiáng)制 order 重新排序也非常困難,因?yàn)槲覀冃枰檎页鲆粋€(gè) item 的所有兄弟節(jié)點(diǎn),這樣會(huì)迫使 Core Data 到磁盤上讀取數(shù)據(jù)。不幸的是,當(dāng)寫這些代碼時(shí),這些問題并沒有立刻弄明白,一些問題只是在寫測(cè)試時(shí)才變得清晰。當(dāng)我重新寫代碼的時(shí)候,我知道了將 Store 類中大部分代碼移到 Item 類中,就這樣,事情變得清楚多了。