在這篇文章中,我們將建立一個(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ā)模型。
檢查錯(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.h和Item.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 中:
大多數(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è)贗tem類中創(chuàng)建一個(gè)自定義的方法來實(shí)現(xiàn):
獲得子節(jié)點(diǎn)數(shù)量的方法很簡(jiǎn)單:
為了支持自動(dòng)更新我們的 table view,我們需要使用 fetched results controller。Fetched results controller 是一個(gè)可以管理取出大量 item 請(qǐng)求的對(duì)象,同時(shí)對(duì)使用 Core Data 的 table view 來說,它也是一個(gè)完美的小伙伴,在下一節(jié)中我們將會(huì)用到:
增加一個(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ì)象,初始化方法如下:
當(dāng)我們?cè)O(shè)置 fetch results controller 時(shí),我們需要設(shè)置自己為 delegate,并且執(zhí)行初始化的 fetch 操作。performFetch:方法經(jīng)常容易被忘了調(diào)用,那么你將得不到結(jié)果(并且不會(huì)出錯(cuò)):
因?yàn)槲覀兊念悓?shí)現(xiàn)了UITableViewDataSource協(xié)議,我們需要實(shí)現(xiàn)相關(guān)的方法。在這兩個(gè)方法中,我們只需要向 fetched results controller 請(qǐng)求需要的信息:
然而,當(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:
創(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中,我們做下面的操作:
在初始化 fetched results controller data source 時(shí),table view 的數(shù)據(jù)源可以被設(shè)置。reuse 標(biāo)識(shí)符匹配在 Storyboard 中相對(duì)應(yīng)的對(duì)象。現(xiàn)在,我們需要實(shí)現(xiàn) delegate 方法:
當(dāng)然,除了設(shè)置 text 的 label 外,你還可以做更多的事情,但是你應(yīng)該已經(jīng)明白了要領(lǐng)。現(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:
監(jiān)聽改變
下一步是確保 table view 會(huì)為新創(chuàng)建的 item 插入一行。有好幾種方法可以做到,但是我們將會(huì)使用 fetched results controller 的代理方法
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ì)變得更漂亮:
使用 Collection View
值得注意的是,fetched results controllers 并非只能用于 table views;你可以將它只用在任何 view 中。因?yàn)樗鼈兪腔?indexPath 的,所以它們能與 collection views 很好的一起工作。由于 collection view 沒有beginUpdates和endUpdates方法,卻有一個(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è)方法:
子 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的代理方法:
一種方法就是在 data source 中設(shè)置 fetched results controller 的代理為nil,這樣就再也不會(huì)收到更新通知了。當(dāng)我們離開paused狀態(tài)時(shí),還需要加上去:
這樣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è)方法:
我們需要通知代理(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)。
現(xiàn)在我們幾乎快完成了。我們可以與 table view 的 cell 交互,并且可以刪除模型對(duì)象。最后一步是實(shí)現(xiàn)一旦模型對(duì)象被刪除后,刪除 table view cell 的必要的代碼。在 data sources 的方法controller:didChangeObject:...中,我們?cè)黾恿硪粋€(gè) if 語句:
增加 Undo 支持
Core Data 優(yōu)點(diǎn)之一就是集成了 undo 支持。我們將為增加晃動(dòng)撤銷功能,第一步就是告訴程序我們可以這么做:
現(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è)方法:
現(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è)置:
基本上就是這樣了。現(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: 的第一行修改成這樣:
現(xiàn)在,當(dāng)用戶抖動(dòng)時(shí),除了普通的 "Undo" 標(biāo)簽外,他將得到更多的上下文環(huán)境。
編輯
編輯目前在示例程序中并不支持,但是這只是一個(gè)改變對(duì)象屬性的問題。比如,改變一個(gè) item 的 title,只需要設(shè)置title屬性就好了。改變fooitem 的 parent,只需要設(shè)置parent屬性為一個(gè)新值bar,所有的東西都將得到更新,bar現(xiàn)在有一個(gè)children為foo,因?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類中,就這樣,事情變得清楚多了。