將待辦事項(xiàng)放入分類中
之前你完成的工作非常不錯(cuò),但是待辦事項(xiàng)分類中其實(shí)不存在任何待辦事項(xiàng)條目。
目前為止,待辦事項(xiàng)和待辦事項(xiàng)分類是相互獨(dú)立的。
讓我們來(lái)把數(shù)據(jù)模型改成下面這個(gè)樣子:

lists數(shù)組還是不變,它包含所有的Checklist對(duì)象,但是這些Checklist每一個(gè)都會(huì)包含自己數(shù)組,其中存放著ChecklistItem對(duì)象。
打開(kāi)Checklist.swift,修改一下:
class Checklist: NSObject {
var name = ""
var items = [ChecklistItem]() //添加這一行
...
僅僅是通過(guò)添加了一行,我們就幾乎萬(wàn)事俱備了。
如果你是處女座,也可以把這一行寫(xiě)成下面這個(gè)樣子:
var items: [ChecklistItem] = [ChecklistItem]()
我個(gè)人不喜歡后者,這種行為被稱為“DRY”-Don’t Repeat Yourself(不要干重復(fù)的事)。感謝swift的類型推斷功能,給我省去了不少事。
有時(shí)你也會(huì)看到寫(xiě)成這個(gè)樣子:
var items: [ChecklistItem] = []
[]這對(duì)方括號(hào)表示,給指定的類型分配一個(gè)空的數(shù)組。
無(wú)論你采用哪種寫(xiě)法,都會(huì)使Checklist對(duì)象包含一個(gè)存儲(chǔ)著ChecklistItem對(duì)象的數(shù)組。最初,這個(gè)數(shù)組是空的。
之前你修改過(guò)AllListsViewController.swift中的prepare(for:sender:),使得用戶點(diǎn)擊某一行時(shí)可以轉(zhuǎn)場(chǎng)到ChecklistViewController,并且將被點(diǎn)擊行的Checklist對(duì)象傳遞過(guò)去。
目前ChecklistViewController仍然從它自己私有的items數(shù)組中得到ChecklistItem對(duì)象。你需要修改一下,讓它從Checklist對(duì)象中的items數(shù)組中讀取ChecklistItem對(duì)象。
打開(kāi)ChecklistViewController.swift,刪掉items實(shí)例變量。
然后在修改以下幾處地方。把任何存在items的地方修改為checklist.items。
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return checklist.items.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ChecklistItem", for: indexPath)
let item = checklist.items[indexPath.row]
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let cell = tableView.cellForRow(at: indexPath) {
let item = checklist.items[indexPath.row]
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) {
checklist.items.remove(at: indexPath.row)
func itemDetailViewController(_ controller: ItemDetailViewController, didFinishAdding item: ChecklistItem) {
let newRowIndex = checklist.items.count
checklist.items.append(item)
func itemDetailViewController(_ controller: ItemDetailViewController, didFinishEditing item: ChecklistItem) {
if let index = checklist.items.index(of: item) {
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
...
controller.itemToEdit = checklist.items[indexPath.row]
...
然后把下面的幾個(gè)方法刪除掉(小帖士,你可以把它們先保存到別的文件里,之后我們會(huì)把它們都粘貼回來(lái),做些小的修改),要?jiǎng)h除的是:
? func documentsDirectory()
? func dataFilePath()
? func saveChecklistItems()
? func loadChecklistItems()
你之前添加這些方法的作用是將checklist items保存到一個(gè)文件中,并且可以在需要的時(shí)候讀取出來(lái)。這些功能已經(jīng)不再是這個(gè)視圖控制器的責(zé)任了。對(duì)app而言,把它們放在Checklist對(duì)象中會(huì)更好。
讀取和存儲(chǔ)數(shù)據(jù)模型對(duì)象放在數(shù)據(jù)模型中會(huì)比放在其他地方要好的多。
在你做這些之前,我們先來(lái)測(cè)試一下剛才的改動(dòng)是不是成功了。Xcode的編譯器應(yīng)該還有幾處報(bào)錯(cuò),因?yàn)槟阏{(diào)用了剛才已經(jīng)被刪除掉的方法,你需要把這些代碼也刪除掉先。
把調(diào)用saveChecklistItems()的行刪掉。
同時(shí)也把init?(coder)刪掉。
然后使用command+B重新編譯一遍程序。
偽造數(shù)據(jù)測(cè)試
讓我們?cè)贑hecklist對(duì)象中造一些數(shù)據(jù),來(lái)測(cè)試一下,這個(gè)新的設(shè)計(jì)是否生效。
在AllListsViewController的init?(coder)方法中你已經(jīng)造了一些Checklist對(duì)象到lists數(shù)組中?,F(xiàn)在我們來(lái)在這個(gè)方法中添加一些新的東西。
在AllListsViewController的init?(coder)方法底部添加以下代碼:
for list in lists {
let item = ChecklistItem()
item.text = "Item for \(list.name)"
list.items.append(item)
}
這里出現(xiàn)了你以前沒(méi)有見(jiàn)過(guò)的東西,for in語(yǔ)句,和if一樣,這是一種特殊的語(yǔ)法結(jié)構(gòu)。
程序語(yǔ)言結(jié)構(gòu)
為了帶大家復(fù)習(xí)一下,我們來(lái)貫穿的看看你已經(jīng)見(jiàn)到過(guò)的編程語(yǔ)言的工具。大多數(shù)現(xiàn)代編程語(yǔ)言都會(huì)提供以下這些最基本的功能:
可以通過(guò)將值存儲(chǔ)入變量的方式,把這個(gè)值保存下來(lái)。有些變量非常簡(jiǎn)單,比如Int和Bool。而有些比較復(fù)雜,它們可以存儲(chǔ)對(duì)象,比如ChecklistItem,UIButton,還有些甚至可以存儲(chǔ)對(duì)象的集合,比如數(shù)組。
可以從變量中讀取值,并且進(jìn)行基本的算術(shù)運(yùn)算,加減乘除等,甚至更復(fù)雜的運(yùn)算,比如比較運(yùn)算等。
可以做出決策,比如你已經(jīng)見(jiàn)過(guò)的if語(yǔ)句,以及switch語(yǔ)句,switch可以將多個(gè)if...else if進(jìn)行簡(jiǎn)化。
可以把功能打包,比如函數(shù)或者方法。你可以調(diào)用這些函數(shù)和方法,并且接收到一個(gè)返回值,并且用這個(gè)值進(jìn)行后面的運(yùn)算。
可以將語(yǔ)句多次甚至無(wú)數(shù)次循環(huán)執(zhí)行。這就是for in語(yǔ)句的作用。還有一些語(yǔ)句也可以完成這個(gè)功能,比如while和repeat。重復(fù)勞動(dòng)是電腦的一大特色。
其他的一切都是基于這些基礎(chǔ)功能之上的,你已經(jīng)見(jiàn)識(shí)過(guò)其中的大部分了,但是循環(huán)還是頭一次見(jiàn)。
如果你對(duì)上面的概念都已經(jīng)了然于心了,那么你離一個(gè)真正的開(kāi)發(fā)者也就不遠(yuǎn)了。如果沒(méi)有的話,請(qǐng)復(fù)習(xí)之前的課程,然后再繼續(xù)往下。
讓我們來(lái)詳細(xì)的分析一下for循環(huán):
for list in lists {
...
}
這個(gè)語(yǔ)句的意思是:對(duì)每一個(gè)在lists數(shù)組中的Checklist對(duì)象,執(zhí)行花括號(hào)內(nèi)的操作。
執(zhí)行循環(huán)的第一次時(shí),list臨時(shí)變量會(huì)得到一個(gè)Birthdays的checklist對(duì)象的引用,因?yàn)檫@是你創(chuàng)建并且添加到數(shù)組中的第一個(gè)Checklist對(duì)象。
在循環(huán)的內(nèi)部,你做了這些事情:
let item = ChecklistItem()
item.text = "Item for \(list.name)"
list.items.append(item)
這應(yīng)該比較熟悉。首先你創(chuàng)建了一個(gè)新的ChecklistItem對(duì)象。然后你設(shè)置它的文本屬性為“Item for Birthdays”,(...)的作用就是插入變量list.name的值到字符串中,而第一個(gè)list.name就是Birthdays。
最后,你將這個(gè)新的ChecklistItem添加到Birthdays checklist對(duì)象的items數(shù)組中。
這是第一次循環(huán)結(jié)束時(shí)發(fā)生的事情,之后for in語(yǔ)句會(huì)發(fā)現(xiàn)lists數(shù)組中還有三個(gè)Checklist對(duì)象,然后對(duì)這三個(gè)對(duì)象做一遍同樣的事情。
執(zhí)行完畢后,lists數(shù)組中已經(jīng)沒(méi)有其他對(duì)象了,此時(shí)循環(huán)結(jié)束。
使用循環(huán)會(huì)使你節(jié)省大量時(shí)間,如果不使用循環(huán)達(dá)到剛才的目的的話,你需要這樣寫(xiě)代碼:
var item = ChecklistItem()
item.text = "Item for Birthdays"
lists[0].items.append(item)
item = ChecklistItem()
item.text = "Item for Groceries"
lists[1].items.append(item)
item = ChecklistItem()
item.text = "Item for Cool Apps"
lists[2].items.append(item)
item = ChecklistItem()
item.text = "Item for To Do"
lists[3].items.append(item)
高下立判,想象一下,如果你有100個(gè)對(duì)象要處理的話,你真的要把這些小段的語(yǔ)句拷貝100次,然后一個(gè)個(gè)去改嗎?
而且大多數(shù)時(shí)候,你根本無(wú)法預(yù)知你要處理幾個(gè)對(duì)象,所以根本無(wú)法挨個(gè)寫(xiě)出它們。通過(guò)使用循環(huán)就不存在這個(gè)問(wèn)題了,有幾個(gè)都無(wú)所謂,讓循環(huán)自己去處理。
也許你已經(jīng)猜到了,循環(huán)和數(shù)組經(jīng)常結(jié)伴出現(xiàn)。
運(yùn)行app?,F(xiàn)在你可以看到每個(gè)分類下都有屬于自己的待辦事項(xiàng)條目了。
好好的體驗(yàn)一下各種功能,刪除、新增都做幾個(gè),看看是不是達(dá)到了想要的效果。

打開(kāi)AllListsViewController.swift,添加以下代碼(記得之前我說(shuō)過(guò)要你把刪掉的幾個(gè)方法先保存到別處嗎?如果你保存了,現(xiàn)在就可以粘貼回來(lái)了。)
func documeentsDirectory() -> URL {
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
return paths[0]
}
func dataFilePath() -> URL {
return documeentsDirectory().appendingPathComponent("Checklists.plist")
}
//用于保存的方法現(xiàn)在叫做saveChecklists()
func saveChecklists() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWith: data)
//這里和之前不同
archiver.encode(lists, forKey: "Checklists")
archiver.finishEncoding()
data.write(to: dataFilePath(), atomically: true)
}
//用于讀取的方法現(xiàn)在叫做loadChecklists()
func loadChecklists() {
let path = dataFilePath()
if let data = try?Data(contentsOf: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
//這里和之前不同
lists = unarchiver.decodeObject(forKey: "Checklists") as! [Checklist]
unarchiver.finishDecoding()
}
這些和你之前在ChecklistViewController中做的幾乎完全相同,除了現(xiàn)在保存的是lists數(shù)組而不是items數(shù)組。注意一下,存儲(chǔ)的鍵值現(xiàn)在是“Checklists”,之前是“ChecklistItem”。同時(shí)保存和讀取的方法名稱也做了相應(yīng)的改變。
同時(shí)改變一下init?(coder)方法:
required init?(coder aDecoder: NSCoder) {
lists = [Checklist]()
super.init(coder: aDecoder)
loadChecklists()
}
我們刪除掉了測(cè)試用的偽造數(shù)據(jù),并且調(diào)用了loadChecklists()方法。
你同時(shí)還要讓Checklist對(duì)象符合NSCoding協(xié)議。
打開(kāi)Checklist.swift,添加NSCoding協(xié)議
class Checklist: NSObject,NSCoding {
回憶一下,NSCoding協(xié)議還需要兩個(gè)方法,分別是init?(coder)和encode(with)。
把它們也添加到Checklist.swift中。
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObject(forKey: "Name") as! String
items = aDecoder.decodeObject(forKey: "Items") as! [ChecklistItem]
super.init()
}
func encode(with aCoder: NSCoder) {
aCoder.encode(name, forKey: "Name")
aCoder.encode(items, forKey: "Items")
}
這樣就實(shí)現(xiàn)了對(duì)name和items的存儲(chǔ)和讀取。
重要:在你運(yùn)行app之前,從模擬器的Documents文件夾中刪除掉Checklists.plist。如果不這樣做的話,app會(huì)掛掉,因?yàn)槲募慕Y(jié)構(gòu)已經(jīng)和現(xiàn)在的數(shù)據(jù)不符合了。
app莫名其妙的掛掉
當(dāng)我第一次寫(xiě)這個(gè)課程,我在修改數(shù)據(jù)模型后再次運(yùn)行app前,忘記了刪除Checklists.plist。但是app好像也能正常運(yùn)行,知道我試著新增一個(gè)新的待辦分類時(shí),app給出了一個(gè)奇怪的報(bào)錯(cuò),并且再也沒(méi)有響應(yīng)了。
一開(kāi)始我窮盡精力檢查了幾遍代碼,沒(méi)有發(fā)現(xiàn)問(wèn)題,后來(lái)我想到可能是這個(gè)文件引起的BUG,然后我刪掉了這個(gè)文件,再次運(yùn)行app,一切運(yùn)行完美。但是我還是不放心,為了確認(rèn)這個(gè)因素,我又將之前保存好的舊版本運(yùn)行了一次,果然,在新增待辦分類時(shí)app掛掉了。此時(shí)就可以確定了,新舊版本的差異就在于Checklists.plist文件的結(jié)構(gòu)。
原因大概是這樣,代碼以某種方式保留著舊的文件,即使此時(shí)數(shù)據(jù)模型已經(jīng)完全不一致了,所以在對(duì)該文件寫(xiě)入內(nèi)容時(shí),就報(bào)錯(cuò)了,后面的一切操作也都無(wú)法繼續(xù)。
你經(jīng)常會(huì)遇到這種莫名其妙的BUG,從你正在做的部分當(dāng)中,根本無(wú)法定位原因,此時(shí)正確的做法就是回到之前做過(guò)的事情中去找原因,類似這種bug在你真正找出原因前,是無(wú)法解決的。
在我們第四個(gè)課程中,有一節(jié)是專門(mén)將如何調(diào)試排出故障的,因?yàn)槟銓?xiě)的代碼肯定會(huì)有bug存在,所以掌握這個(gè)技能是必須的??焖俣ㄎ徊⑶腋齜ug,是成熟的開(kāi)發(fā)者的必備技能。
刪除掉舊的Checklists.plist后,運(yùn)行app,試試各種功能是否正常運(yùn)行。
然后退出app(使用Stop按鈕中斷app),再次運(yùn)行,你會(huì)發(fā)現(xiàn)列表全部空空如也。你添加的所有東西都沒(méi)了。
你可以添加任何你想添加的事項(xiàng),但是什么都保存不下來(lái)。
這是怎么回事呢?
不同的存儲(chǔ)
之前,每當(dāng)用戶新增、刪除條目,或者點(diǎn)擊對(duì)勾符號(hào)都會(huì)調(diào)用存儲(chǔ),將Checklists.plist更新一遍,這就是之前ChecklistViewController具備的功能。
然而,你將這些存儲(chǔ)邏輯移放到了AllListsViewController中,這樣,你如何能保證待辦事項(xiàng)條目被保存呢?AllListsViewController甚至都不知道對(duì)勾符號(hào)的存在。
你可以給ChecklistViewController一個(gè)引用到AllListsViewController,當(dāng)用戶做出改變時(shí),調(diào)用saveChecklists()方法,但是這樣會(huì)形成一個(gè)所謂的父子從屬關(guān)系,這是你極力需要避免的情況(循環(huán)引用,記得嗎?)
父親和它們的孩子
在軟件開(kāi)發(fā)中父親和孩子是非常常見(jiàn)的術(shù)語(yǔ)。
父親就是在某些層級(jí)上比較高位的對(duì)象;孩子就是在這個(gè)層級(jí)中覺(jué)低位的對(duì)象。
在我們的這個(gè)情況中,“層級(jí)”就是app中不同界面的間的導(dǎo)航流轉(zhuǎn)。
All Lists界面是Checklist界面的父級(jí),因?yàn)锳ll Lists先“出生”。每次轉(zhuǎn)場(chǎng)時(shí)它創(chuàng)造了ChecklistViewController這個(gè)新的孩子。
同樣的,All Lists也是List Detail界面的父級(jí)。而Item Detail界面是Checklist view controller的子級(jí)。

總體而言,父級(jí)對(duì)子級(jí)了如指掌是沒(méi)問(wèn)題的,而子級(jí)對(duì)父級(jí)則不是這樣,就像真實(shí)生活中一樣,父母總是有些可怕的秘密不像讓孩子知道。
結(jié)果就是,你不想讓父級(jí)對(duì)象依賴它們的子級(jí)對(duì)象,所以讓ChecklistViewController通知AllListsViewController做事,是不對(duì)的。
你也許會(huì)想:噢,我可以使用一個(gè)委托。確實(shí)可以,而且如果你真的有這個(gè)意識(shí)的話,我會(huì)感到非常自豪,證明我沒(méi)有白教,但是我們不會(huì)這樣做,取而代之的是,我們要回頭來(lái)重新考慮一下我們的存儲(chǔ)策略。
我們真的有必要實(shí)時(shí)的進(jìn)行數(shù)據(jù)保存嗎?當(dāng)app運(yùn)行期間,數(shù)據(jù)模型在內(nèi)存中不斷的被刷新。
你需要從文件中讀取信息的唯一時(shí)刻就是當(dāng)app啟動(dòng)的時(shí)候,而之后就不需要再次讀取了。從app運(yùn)行后起,你所做的一切操作,都會(huì)都會(huì)被內(nèi)存記下來(lái)。
但是當(dāng)改變發(fā)生的時(shí)候,之前存儲(chǔ)的文件就過(guò)時(shí)了。這就是為什么我們之前把這些改變存儲(chǔ)了下來(lái),保持文件和內(nèi)存的實(shí)時(shí)同步。
但是實(shí)際上,我們讀取文件僅發(fā)生在app啟動(dòng)時(shí),并不會(huì)在app運(yùn)行期間實(shí)時(shí)的讀取文件,所以我們只要在app中斷時(shí)保存一次就可以了,其余的時(shí)間,讓內(nèi)存自動(dòng)保存數(shù)據(jù)沒(méi)有任何問(wèn)題。
換而言之,你僅需確保,在app中斷前,對(duì)數(shù)據(jù)進(jìn)行一次保存。
這樣做并不是僅僅為了提高效率,而是我們的app實(shí)現(xiàn)的是一個(gè)非常簡(jiǎn)單的功能,并不需要實(shí)時(shí)存儲(chǔ)(像word這類軟件確實(shí)需要實(shí)時(shí)存儲(chǔ))。
有以下三種情況,會(huì)導(dǎo)致app中斷:
1、早期的iOS系統(tǒng)并不支持多任務(wù),當(dāng)用戶運(yùn)行app期間,接進(jìn)來(lái)了一個(gè)電話,就會(huì)殺掉正在運(yùn)行的app的進(jìn)程。從iOS 4開(kāi)始就不會(huì)這樣了,接進(jìn)電話時(shí),會(huì)把正在運(yùn)行的app切換到后臺(tái)。但是即使是比較新的iOS版本,也會(huì)在某些情況下殺掉正在運(yùn)行的app進(jìn)程,比如說(shuō)打開(kāi)的應(yīng)用太多,內(nèi)存耗盡了。
2、當(dāng)app被切換到后臺(tái)以后。iOS系統(tǒng)在大多數(shù)時(shí)間內(nèi)都會(huì)保證app的存活,它們的數(shù)據(jù)被凍結(jié)起來(lái),只是不在進(jìn)行運(yùn)算。當(dāng)你從新把a(bǔ)pp從后臺(tái)切換回來(lái)以后,app繼續(xù)正常運(yùn)行。
而有時(shí),iOS需要更大的內(nèi)存給其他的app,多數(shù)時(shí)候是游戲類app,為了保證內(nèi)存夠用,iOS會(huì)殺掉后臺(tái)app,并且把內(nèi)存清理出來(lái),而這一切,都不會(huì)提前通知這個(gè)app。
3、app異常崩潰。發(fā)現(xiàn)app崩潰很容易,但是處理起來(lái)很難,而且可能會(huì)在處理的過(guò)程中,使事情更加復(fù)雜。(避免app崩潰最好的辦法就是代碼不要寫(xiě)錯(cuò))
幸運(yùn)的是,在諸如中斷和切換至后臺(tái)這些動(dòng)作發(fā)生前,iOS系統(tǒng)都會(huì)禮貌性的通知app一下。
你可以在聽(tīng)到這些通知的這一時(shí)刻,對(duì)數(shù)據(jù)進(jìn)行保存。這樣就可以保證每次app中斷前,你都可以及時(shí)保存數(shù)據(jù)。
處理這些通知的地方是在application delegate中,你之前可能沒(méi)關(guān)注過(guò)它,但是每個(gè)app都有一個(gè)application delegate。從名字就可以看出來(lái),這是一個(gè)用于app全部通知的委托。
也是你接受到app中斷和app被切換至后臺(tái)通知的地方。
事實(shí)上,你打開(kāi)AppDelegate.swift,你就會(huì)看到這個(gè)方法:
func applicationDidEnterBackground(_ application: UIApplication)
和
func applicationWillTerminate(_ application: UIApplication)
還有一些其他方法,但是現(xiàn)在你需要的就是這兩個(gè)(Xcode已經(jīng)為這些方法寫(xiě)好了清晰的注釋,所以你可以輕易的知曉它們的作用)。
現(xiàn)在的問(wèn)題是,你如何從這些委托方法中調(diào)用AllListsViewController的saveChecklists()。app delegate并不知道AllListsViewController的存在。
你需要介紹它們倆相互認(rèn)識(shí)一下。
打開(kāi)AppDelegate.swift,添加一個(gè)方法進(jìn)去:
func saveData() {
let navigationController = window!.rootViewController as! UINavigationController
let controller = navigationController.viewControllers[0] as! AllListsViewController
controller.saveChecklists()
}
saveData() 方法從window屬性中找到包含故事模版的UIWindow對(duì)象。
UIWindow是你app中所有視圖中的最高層級(jí)的視圖。在你的app中僅存在一個(gè)UIWindow對(duì)象,與桌面軟件不同,桌面軟件通常有多個(gè)window。
練習(xí):你可以解釋 為什么window后面要帶一個(gè)感嘆號(hào)嗎?
可選型解包
在AppDelegate.swift頂部,你可以看到window被聲明為一個(gè)可選型:
var window: UIWindow?
給可選型解包,你一般會(huì)使用if let語(yǔ)句:
if let w = window {
// if window is not nil, w is the real UIWindow object
let navigationController = w.rootViewController
}
也可以簡(jiǎn)寫(xiě)為可選型鏈接的形式:
let navigationController = window?.rootViewController
如果window為nil,則app都不會(huì)去看剩余的代碼,并且navigationController也會(huì)為空。
因?yàn)閍pp使用了一個(gè)故事模版(大多數(shù)都會(huì)),所以你可以保證window絕對(duì)不會(huì)為nil,即使它是一個(gè)可選型。UIKit可以保證在app啟動(dòng)時(shí),在window變量?jī)?nèi)部有一個(gè)有效的指向UIWindow對(duì)象的引用。
那么為什么它會(huì)是一個(gè)可選型呢?當(dāng)app啟動(dòng),并且加載故事模版時(shí),會(huì)有一個(gè)短暫的瞬間window屬性為nil。如果一個(gè)變量可以為空,不管這個(gè)時(shí)間有多短暫,Swift都會(huì)要求將它申明為可選型。
如果你可以確保一個(gè)可選型不會(huì)為nil,那么當(dāng)你使用它的時(shí)候,你就可以用強(qiáng)制解包的方式,在可選型變量的后面加一個(gè)感嘆號(hào)就是強(qiáng)制解包的意思:
let navigationController = window!.rootViewController
你在saveData()方法中用的就是強(qiáng)制解包。強(qiáng)制解包是處理可選型的最簡(jiǎn)單的方式,但是它會(huì)帶來(lái)很大的潛在危險(xiǎn):如果你的判斷有誤,當(dāng)強(qiáng)制解包的時(shí)候可選型為nil,那么app就會(huì)掛掉。所以在使用強(qiáng)制解包的時(shí)候一定要三思。
其實(shí)在Item Detail和List Detail中你從UITextField中讀取文本的時(shí)候 ,你就已經(jīng)使用過(guò)強(qiáng)制解包了。UITextField的text屬性就是一個(gè)可選型,但是它永遠(yuǎn)不會(huì)為nil,所以你可以用textField.text!來(lái)進(jìn)行強(qiáng)制解包,這個(gè)感嘆號(hào)就是強(qiáng)制解包的意思,它可以把可選型轉(zhuǎn)換為常規(guī)的變量。
通常你不需要對(duì)UIWindow做任何操作,但是在這種情況下,你不得不向它請(qǐng)求訪問(wèn)它的rootViewController。root或者initial視圖控制器,是故事模版中最開(kāi)始的界面,navigation controller就在它的左邊。
你可以在界面建造器中看到這一點(diǎn),因?yàn)閚avigation controller左邊有一個(gè)大箭頭指著它,就是下面這個(gè):

在navigation controller的屬性檢查器中,有一個(gè)Is Initial View的選框是選中狀態(tài),這個(gè)選框和大箭頭是一個(gè)意思。在綱要面板中它被稱作故事模版的起點(diǎn)。
一旦你有了這個(gè)navigation controller,你就可以找到AllListsViewController。畢竟這些視圖控制器都是被嵌入到navigation controller內(nèi)部的。
不幸的是,UINavgationController沒(méi)有自己的“rootViewController”屬性,所以你需要自己從視圖控制器的數(shù)組中找到它:
let controller = navigationController.viewControllers[0] as! AllListsViewController
和平常一樣,這里要使用類型扮演,因?yàn)橐晥D控制器數(shù)組對(duì)其他你的視圖控制器類型一無(wú)所知。一旦你有了一個(gè)指向AllListsViewController的引用,你就可以調(diào)用它的saveChecklists()方法了。
從window到navigation controller,直到找到你想要的視圖控制器花了不少功夫,但這就是iOS開(kāi)發(fā)者的必修課。

??:順便說(shuō)一下,UINavigationController有一個(gè)topViewController屬性,但是在這里你不能使用它:top view controller是正在顯示中的界面,如果用戶正在對(duì)待辦事項(xiàng)條目進(jìn)行操作的話,這個(gè)top view controller就是ChecklistViewController,而它無(wú)法處理saveChecklists()方法,app就會(huì)掛掉。
在applicationDidEnterBackground()和applicationWillTerminate() 方法中調(diào)用saveData()方法:
func applicationDidEnterBackground(_ application: UIApplication) {
saveData()
}
func applicationWillTerminate(_ application: UIApplication) {
saveData()
}
運(yùn)行app,做些操作,包括更改一下對(duì)勾符號(hào)的狀態(tài)。
可以使用Shift+command+H鍵或者模擬器菜單Hardware->Home來(lái)使模擬器中的app切換到后臺(tái),就像在iPhone上按下Home鍵一樣。
順便看看Document文件中,新的Checklist.plist文件已經(jīng)生成了。
按下Stop按鈕可以中斷app,然后再次運(yùn)行app,看看保存結(jié)果是否還存在。
??:Xcode的Stop按鈕
非常重要:當(dāng)你點(diǎn)擊Xcode的Stop按鈕后,application delegate不會(huì)從applicationWillTerminate()中接收到消息。這和真實(shí)的手機(jī)使用情況不同。
因此為了測(cè)試這個(gè)存儲(chǔ)效果,你需要先將app切換到后臺(tái),如果在你點(diǎn)擊Stop按鈕前,沒(méi)有使用Shift+command+H把a(bǔ)pp切換到后臺(tái),那么數(shù)據(jù)不會(huì)被保存下來(lái)。