改進(jìn)數(shù)據(jù)模型
上面的代碼已經(jīng)可以工作了秉剑,但是你仍舊可以做的更好一些。你為Checklist和ChecklistItem做了數(shù)據(jù)模型對(duì)象械筛,但是讀取和存儲(chǔ)Checklists.plist文件的代碼目前位于AllListsViewController中捎泻。根據(jù)良好的代碼編寫原則,我們應(yīng)該把它也放入數(shù)據(jù)模型中埋哟。
我喜歡為我的許多app都創(chuàng)建一個(gè)頂級(jí)的數(shù)據(jù)模型笆豁。對(duì)于這個(gè)app而言郎汪,數(shù)據(jù)模型會(huì)包含Checklist對(duì)象的數(shù)組。你可以將用于讀取和存儲(chǔ)的代碼移到新的數(shù)據(jù)模型對(duì)象中闯狱。
新建一個(gè)Swift文件煞赢。將它保存為DataModel.swift(你不需要將它設(shè)置為任何東西的子類)
在DataModel.swift中添加以下代碼:
import Foundation
class DataModel {
var lists = [Checklist]()
}
這樣就定義了一個(gè)新的數(shù)據(jù)模型對(duì)象并且給了它一個(gè)lists屬性。
不像Checklist和ChecklistItem哄孤,數(shù)據(jù)模型不需要建立在NSObject之上照筑。它也不需要符合NSCoding協(xié)議。
數(shù)據(jù)模型會(huì)接管讀取及保存AllListsViewController中的待辦事項(xiàng)瘦陈。
把下面的幾個(gè)方法從AllListsViewController.swift中剪切出來凝危,再粘貼到DataModel.swift中:
func documentsDirectory()
func dataFilePath()
func saveChecklists()
func loadChecklists()
并且在DataModel.swift添加一個(gè)init()方法:
init() {
loadChecklists()
}
這樣就保證了,一旦DataModel對(duì)象被創(chuàng)建晨逝,它就會(huì)去讀取Checklists.plist蛾默。
lists實(shí)例變量在聲明時(shí)就已經(jīng)有了一個(gè)初始值,所以在init()方法中捉貌,我們不用管它趴生。
同時(shí),你也不需要調(diào)用super.init()昏翰,因?yàn)镈ataModel沒有父類苍匆。
回到AllListsViewController.swift,并且做出如下改動(dòng):
移除lists實(shí)例變量
移除init?(coder)方法
添加一個(gè)新的實(shí)例變量:
var dataModel: DataModel!
這里的感嘆號(hào)是必須的,因?yàn)楫?dāng)app啟動(dòng)時(shí)的一個(gè)短暫時(shí)間內(nèi)dataModel會(huì)為nil棚菊。但是并不需要把dataModel聲明為可選型浸踩,帶上一個(gè)問號(hào),因?yàn)橐坏ヾataModel有值后统求,就再也不會(huì)為nil了检碗。
此時(shí)Xcode應(yīng)該為你指出了AllListsViewController.swift中有幾處報(bào)錯(cuò)。你不能再直接引用lists變量了码邻,因?yàn)樗呀?jīng)被你刪掉了折剃。取而代之的是,你要向DataModel請(qǐng)求它的lists屬性像屋。
任何AllListsViewController中調(diào)用了lists的地方怕犁,都要修改為dataModel.lists,一共需要修改這么幾處地方己莺。
tableView(numberOfRowsInSection)
tableView(cellForRowAt)
tableView(didSelectRowAt)
tableView(commit, forRowAt)
tableView(accessoryButtonTappedForRowWith) listDetailViewController(didFinishAdding) listDetailViewController(didFinishEditing)
要改的地方真多奏甫,還好改動(dòng)都不大。
你創(chuàng)建了一個(gè)新的數(shù)據(jù)模型凌受,它包含Checklist數(shù)組阵子,并且可以保存和讀取checklists和其中的數(shù)據(jù)。
而AllListsViewController已經(jīng)不在使用自己的數(shù)組胜蛉,轉(zhuǎn)而使用DataModel對(duì)象挠进,通過讀取dataModel的屬性實(shí)現(xiàn)色乾。
但是DataModel是在哪里創(chuàng)建的?在整個(gè)代碼中還沒有說明dataModel = DataModel()领突。
做這件事情最佳的地方就是在app delegate中暖璧。你可以認(rèn)為app delegate是整個(gè)app中最高層級(jí)的地方。因此攘须,讓它擁有這個(gè)數(shù)據(jù)模型是最合理的。
然后app delegate向任何需要DataModel的視圖控制器傳遞這一對(duì)象殴泰。
打開AppDelegate.swift于宙,添加一個(gè)新的屬性:
let dataModel = DataModel()
這樣就創(chuàng)建了一個(gè)DataModel對(duì)象,并且將它放入了一個(gè)名為dataModel的常量里悍汛。
雖然AllListsViewController中已經(jīng)有了一個(gè)叫做dataModel的實(shí)例變量捞魁,但是它們倆是獨(dú)立存在的。這里你僅僅是將DataModel對(duì)象放入AppDelegate的dataModel屬性中离咐。
輕微修改一下saveData方法:
func saveData() {
dataModel.saveChecklists()
}
如果你現(xiàn)在運(yùn)行app的話谱俭,app會(huì)掛掉,因?yàn)锳llListsViewController引用的DataModel此時(shí)還是nil宵蛀。我告訴過你昆著,nil不是啥好事。
向AllListsViewController分享DataModel實(shí)例最佳的地方就是在application(didFinishLaunchingWithOptions) 方法中术陶,這個(gè)方法在app一啟動(dòng)時(shí)就會(huì)被調(diào)用凑懂。
將這個(gè)方法修改為:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
let navigationController = window!.rootViewController as! UINavigationController
let controller = navigationController.viewControllers[0] as! AllListsViewController
controller.dataModel = dataModel
return true
}
這里通過故事模版找到AllListsViewController(和之前一樣),然后設(shè)置它的dataModel屬性∥喙現(xiàn)在All Lists界面又可以讀取Checklist對(duì)象了接谨。
由于我們改了太多東西,需要清理一下緩存塘匣,選擇菜單Product->Clean清理一下脓豪,然后運(yùn)行app〖陕保看看一切是否工作正常扫夜。
我仍然對(duì)var和let感到困惑不解
如果var用于定義變量,而let用于定義常量驰徊,那么為什么你可以在AppDelegate中這樣使用呢历谍?
let dataModel = DataModel()
你會(huì)想,如果某樣?xùn)|西是常量辣垒,那么它就是無法被改變的望侈,對(duì)嗎?
那么為什么app會(huì)允許你向DataModel中添加新的Checklist對(duì)象呢勋桶?很明顯脱衙,DataModel是隨時(shí)在發(fā)生變化的侥猬。
這里是一個(gè)小把戲:Swift對(duì)值類型和引用類型做了區(qū)分,并且let在這兩者上的工作方式有些不同捐韩。
比如Int(整數(shù)型)就是一個(gè)值類型退唠,一旦你創(chuàng)建了一個(gè)Int型的常量,那么之后你就不能再改變它的值:
let i = 100
i = 200 //錯(cuò)誤
i += 1 //錯(cuò)誤
var j = 100
j = 200 //正確
j += 1 //正確
這個(gè)原理對(duì)其他類型Float荤胁、String瞧预、甚至Array(數(shù)組)都是成立的。它們都是所謂的值類型仅政,因?yàn)樽兞亢统A恐苯哟鎯?chǔ)它們的值垢油。
當(dāng)你分配一個(gè)變量的內(nèi)容到另一個(gè)時(shí),值會(huì)被拷貝到新的變量中:
var s = "hello"
var u = s // u 拷貝了 "hello"
s += " there" // s 和 u 已經(jīng)不同了
但是你用class關(guān)鍵字定義的對(duì)象是引用類型(比如DataModel)圆丹,常量和變量并不會(huì)實(shí)際的包含這個(gè)對(duì)象滩愁,而是包含一個(gè)到這個(gè)對(duì)象的引用。
var d = DataModel()
var e = d // e和d引用同一個(gè)對(duì)象
d.lists.remove(at: 0) // 改變d的時(shí)候e同時(shí)會(huì)被改變
把上面的變量改為常量辫封,作用是一樣的:
let d = DataModel()
let e = d //e和d引用同一個(gè)對(duì)象
d.lists.remove(at: 0) // 改變d的時(shí)候e同時(shí)會(huì)被改變
所以硝枉,對(duì)引用類型而言,變量和常量有什么區(qū)別呢倦微?
當(dāng)你使用let時(shí)妻味,并不能把對(duì)象變成常量,但是可以把到這個(gè)對(duì)象的引用變成常量欣福,這意思就是說弧可,你不能像下面這樣做:
let d = DataModel()
d = someOtherDataModel // 錯(cuò)誤: 你不能改變這個(gè)引用
這個(gè)常量d永遠(yuǎn)不能指向其他對(duì)象,但是對(duì)象本身可以發(fā)生改變劣欢。
理解起來可能會(huì)有些困難棕诵,但是值類型和引用類型的區(qū)別在軟件開發(fā)中非常重要,花再大的精力你也必須掌握它凿将。
我的建議是校套,你在寫代碼的時(shí)候全部使用let,直到編譯器提示你要修改為var的時(shí)候再改成var牧抵。
使用用戶缺省來記住界面
你現(xiàn)在擁有了一個(gè)app笛匙,可以使你創(chuàng)建待辦事項(xiàng)分類,并且在每個(gè)分類中添加具體的待辦事項(xiàng)犀变。所有這些數(shù)據(jù)都被長期存儲(chǔ)妹孙,甚至app中斷后都不會(huì)丟失數(shù)據(jù)。
但是你仍然可以做一些優(yōu)化工作获枝。
想象一下蠢正,用戶正在生日分類操作時(shí),因?yàn)橛衅渌虑袚Q到了另外的app上省店,我們的app就被切換到后臺(tái)了嚣崭。這就有可能在某一時(shí)刻內(nèi)存會(huì)將app移出去笨触,app就中斷了。
當(dāng)用戶過會(huì)在切換回來時(shí)雹舀,app并不是停留在生日分類上芦劣,而是在主界面。因?yàn)閍pp中斷后并不是從離開的地方重新開始说榆,而是直接重新開始虚吟。
也許你會(huì)忽視這一點(diǎn),畢竟這種情況并不經(jīng)常發(fā)生(除非你打開了好幾個(gè)游戲app)签财,但是偉大的iOS產(chǎn)品就在于細(xì)節(jié)串慰。
幸運(yùn)的是,實(shí)現(xiàn)這個(gè)功能非常容易荠卷。
你可以把這些信息保存在Checklists.plist文件中模庐,但是更加簡單的一個(gè)方式就是使用UserDefault對(duì)象烛愧。
UserDefault的工作原理和字典類似油宜,字典是一種存儲(chǔ)配對(duì)鍵值的集合類型的對(duì)象。你已經(jīng)見過了數(shù)組這種集合類型的對(duì)象怜姿,可以像列表一樣存儲(chǔ)對(duì)象慎冤。字典也是一種非常常見的集合類型,如下圖所示:
Swift中字典是有Dictionary對(duì)象實(shí)現(xiàn)的沧卢。
你可以把對(duì)象放入字典并且以鍵為索引引用它的值蚁堤。這也正是Info.plist的工作方式。
Info.plist文件被iOS系統(tǒng)讀取進(jìn)一個(gè)字典但狭,使用多種鍵(左邊的那一排)來獲取值(右邊的那一排)披诗。鍵通常都是字符,但是值可以是任何類型的對(duì)象立磁。
公平的講呈队,UserDefault不是一個(gè)真正的字典,但是與字典非常相似唱歧。
當(dāng)你向UserDefault中插入新的值時(shí)宪摧,它們被保存在你app沙盒的某個(gè)地方,所以這些值甚至可以在app中斷后還存在颅崩。
你不會(huì)在UserDefault中存儲(chǔ)大量的數(shù)據(jù)几于,但是對(duì)于一些小東西,比如設(shè)置類的沿后,或者對(duì)屏幕瀏覽記錄做個(gè)保存沿彭,UserDefault是個(gè)不錯(cuò)的選擇。
你要做的事情是:
1尖滚、在主界面(AllListsViewController)到checklist(ChecklistViewController)的轉(zhuǎn)場上膝蜈,你要為被選擇的行寫一個(gè)索引放入U(xiǎn)serDefault锅移。通過這種方式,你就可以記住被激活的checklist是哪一個(gè)饱搏。
你可以保存checklist的名稱來代替保存這一行的索引值非剃,但是假如有兩行的checklist名稱一樣會(huì)發(fā)生什么呢?雖然不太可能推沸,但是也不能保證沒有备绽。使用行的索引可以確保你總是能得到唯一的一行。
2鬓催、當(dāng)用戶點(diǎn)擊back按鈕回到主界面后肺素,你要移除掉UserDefault中的值。你可以通過設(shè)置值為-1來表示沒有值宇驾。
為什么是-1呢倍靡?你是從0開始計(jì)數(shù),所以你不能使用0课舍。正數(shù)也不在考慮范圍內(nèi)塌西,除非你使用1000000這樣的值,因?yàn)榛旧嫌脩舨粫?huì)增加這么多行筝尾,但是理論上這樣也不妥當(dāng)捡需。而-1不是一個(gè)有效的索引,并且負(fù)數(shù)看起來會(huì)非常醒目筹淫,用來做特殊標(biāo)示非常合適站辉。(如果你在想,為什么不能用一個(gè)可選型呢损姜?nil就代表沒有饰剥,這是一個(gè)好問題,答案是很令人傷心的摧阅,因?yàn)閁serDefault不能處理可選型)
3汰蓉、如果app啟動(dòng)并且UserDefault的值不是-1的話,那就是說用戶之前停留在查看某個(gè)checklist內(nèi)容的行為上逸尖,這時(shí)你要用代碼轉(zhuǎn)場到ChecklistViewController中相應(yīng)的行上古沥。
說了這么多,我們還不如立刻開始上手做一做娇跟。
讓我們從主界面的轉(zhuǎn)場開始岩齿〕善瑁回憶一下這個(gè)轉(zhuǎn)場是由代碼觸發(fā)垢袱,而不是由storyboard。
打開AllListsViewController.swift,將tableView(didSelectRowAt)方法修改為:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
//添加這一行
UserDefaults.standard.set(indexPath.row, forKey: "ChecklistIndex")
let checklist = dataModel.lists[indexPath.row]
performSegue(withIdentifier: "ShowChecklist", sender: checklist)
}
除了添加一行以外袜香,其他部分不要?jiǎng)樱@樣你就將被選擇的這一行的index存儲(chǔ)到UserDefaults下的“ChecklistIndex”中了乞封。
為了識(shí)別用戶是否點(diǎn)擊了導(dǎo)航欄上的back按鈕做裙,你需要成為導(dǎo)航控制器的委托。成為委托意味著導(dǎo)航棧堆中推入或者彈出視圖控制器時(shí)肃晚,你都會(huì)得到一個(gè)通知锚贱。
理想的添加委托的地方是在AllListsViewController中。
打開AllListsViewController.swift,添加委托協(xié)議:
class AllListsViewController: UITableViewController,ListDetailViewControllerDelegate,UINavigationControllerDelegate {
和你看到的一樣关串,一個(gè)視圖控制器可以同時(shí)作為許多對(duì)象的委托拧廊。
AllListsViewController現(xiàn)在同時(shí)是ListDetailViewController和UINavigationController的委托了。
在AllListsViewController.swift的底部添加委托方法:
func navigationController(_ navigationController: UINavigationController, willShow viewController: UIViewController, animated: Bool) {
//back按鈕被點(diǎn)擊了嗎晋修?
if viewController === self {
UserDefaults.standard.set(-1, forKey: "ChecklistIndex")
}
無論何時(shí)吧碾,導(dǎo)航控制器中出現(xiàn)一個(gè)新的視圖的時(shí)候,這個(gè)方法都會(huì)被調(diào)用墓卦。
如果back按鈕被點(diǎn)擊了倦春,新的視圖控制器是AllListsViewController自己,這時(shí)你設(shè)置“ChecklistIndex”的值為-1落剪,意味著此時(shí)沒有任何一個(gè)具體的待辦條目被選中睁本。
相等和相同
為了確定AllListsViewController是否是最新的一個(gè)視圖,你寫的代碼為:
if viewController === self
這并不是印刷錯(cuò)誤著榴,這里就是三個(gè)“=”號(hào)添履。
之前你比較兩個(gè)對(duì)象的時(shí)候用的都是兩個(gè)等于符號(hào)屁倔,比如:
if segue.indentifier == "AddItem" {
你肯定很像知道三個(gè)等于號(hào)和兩個(gè)等于號(hào)到底有什么不同脑又。它們之間的差別非常微妙,但是卻是關(guān)于身份同一性的重要問題锐借。(嚴(yán)格來說问麸,這是個(gè)哲學(xué)問題,T T)
如果你使用==钞翔,你是在檢查兩個(gè)變量是否有同一個(gè)值严卖。
而當(dāng)你使用===,你是在檢查兩個(gè)變量是否引用了同一個(gè)對(duì)象布轿。
想象一下哮笆,都兩個(gè)人,名字都叫張三汰扭。他們是兩個(gè)不同的人稠肘,但是名字一樣。
如果你用===來比較的話萝毛,if 張三1 === 張三2项阴,那么返回結(jié)果是不同,因?yàn)樗麄兪莾蓚€(gè)人笆包。但是如果你用==环揽,if 張三1 == 張三2略荡,那么返回結(jié)果就是相同,因?yàn)樗麄兊拿郑ㄖ担┮粯印?/p>
另一方面講歉胶,假如兩個(gè)張三是來自不同時(shí)空的同一個(gè)張三汛兜,那么if 張三1 === 張三2也會(huì)返回成功。
順便說一下通今,上面的代碼如果你寫成了兩個(gè)等號(hào)序无,app也一樣會(huì)運(yùn)行正常:
if viewController == self
對(duì)于視圖控制器而言,你使用兩個(gè)等號(hào)它也會(huì)去比較引用而不是值衡创,就像三個(gè)等號(hào)一樣帝嗡,但是技術(shù)上講使用三個(gè)等號(hào)顯得更加專業(yè)。
在app啟動(dòng)時(shí)檢查那條待辦事項(xiàng)被選中了璃氢,并且通過代碼轉(zhuǎn)場過去哟玷,還剩一點(diǎn)工作要做。我們會(huì)在viewDidAppear()方法中實(shí)現(xiàn)這件事一也。
打開AllListsViewController.swift巢寡,添加這個(gè)方法:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
navigationController?.delegate = self
let index = UserDefaults.standard.integer(forKey: "ChecklistIndex")
if index != -1 {
let checklist = dataModel.lists[index]
performSegue(withIdentifier: "ShowChecklist", sender: checklist)
}
}
當(dāng)視圖控制器可視化的時(shí)候,UIKit會(huì)自動(dòng)調(diào)用這個(gè)方法椰苟。
首先抑月,視圖控制器使自己成為導(dǎo)航控制器的委托。
每個(gè)視圖控制器都有一個(gè)內(nèi)建的導(dǎo)航控制器屬性舆蝴,你可以使用navigationController?.delegate讀取它谦絮,因?yàn)樗莻€(gè)可選型,所以你要使用一個(gè)問號(hào)洁仗。
你也可以使用感嘆號(hào)來代替問號(hào)层皱。區(qū)別在于如果這個(gè)視圖控制器從不在外部展示一個(gè)導(dǎo)航控制器的話,那么使用感嘆號(hào)會(huì)使app掛掉赠潦。而使用問號(hào)則不會(huì)叫胖,使用問號(hào)時(shí)僅僅會(huì)使得這一行被忽略。
然后它檢查UserDefaults她奥,看看是否需要執(zhí)行轉(zhuǎn)場瓮增。
如果“ChecklistIndex”的值為-1,那就是說在app中斷前哩俭,用戶是停留在主界面上的绷跑,這時(shí)你不需要做任何事情。
然而如果“ChecklistIndex”的值不是-1的話携茂,就是說用戶在app中斷前是停留在某條待辦事項(xiàng)上的你踩,你需要轉(zhuǎn)場到相應(yīng)的地方。和以前一樣,你把Checklist相關(guān)的對(duì)象放到sender中:performSegue(withIdentifier,sender)带膜。
!=,這個(gè)符號(hào)的意思是“不等于”吩谦。和==操作符正好相反。就是算術(shù)中的≠膝藕。有些語言使用的是<>,但是Swift中不是式廷。
??:這里發(fā)生的事情并不是太一目了然。
viewDidAppear()并不是當(dāng)app啟動(dòng)時(shí)才被調(diào)用芭挽,每次導(dǎo)航控制器將主界面滑動(dòng)回視圖中時(shí)也會(huì)被調(diào)用滑废。
檢查checklist界面是否被恢復(fù),應(yīng)該僅在app啟動(dòng)時(shí)發(fā)生袜爪,所以為什么你將這段邏輯放入viewDidAppear()中呢蠕趁?如果它被調(diào)用的那么頻繁的話。
理由如下:
AllListsViewController的界面可視化前你并不想調(diào)用navigationController(willShow...)委托方法辛馆,因?yàn)檫@樣做會(huì)使得你在保存舊的界面前俺陋,“ChecklistIndex”就會(huì)被重寫為-1。
通過等待AllListsViewController可視化之后再將其注冊(cè)為導(dǎo)航控制器的委托昙篙,可以避免這個(gè)問題的發(fā)生腊状。viewDidAppear()正適合這個(gè)時(shí)機(jī)。
然而苔可,前面提到了缴挖,viewDidAppear()當(dāng)用戶點(diǎn)擊back按鈕回到All Lists界面時(shí)它也會(huì)被調(diào)用。它不會(huì)造成負(fù)面影響焚辅,比如重復(fù)觸發(fā)轉(zhuǎn)場映屋。
當(dāng)用戶點(diǎn)擊back按鈕時(shí)導(dǎo)航控制器調(diào)用navigationController(willShow...)方法,這件事發(fā)生在viewDidAppear()被調(diào)用之前法焰。這樣委托方法總是能及時(shí)的將ChecklistIndex重置為-1秧荆,而viewDidAppear()絕不會(huì)重復(fù)觸發(fā)轉(zhuǎn)場倔毙。
結(jié)果就是埃仪,每次app啟動(dòng)時(shí)viewDidAppear()內(nèi)的邏輯才會(huì)被執(zhí)行,還有一些方法可以達(dá)到同樣目的陕赃,但是這個(gè)最簡單卵蛉。
你能理解這整個(gè)過程了嗎?不要急躁么库,保持平常心傻丝。如果你想確實(shí)的觀察一下發(fā)生了什么,可以在這些方法中添加一些print()方法來觀察一下诉儒,所謂百聞不如一見葡缰。
核實(shí)一下所有UserDefaults語句中使用的鍵值都是一樣的,應(yīng)該是“ChecklistIndex”。如果其中一個(gè)錯(cuò)了的話泛释,UserDefault會(huì)產(chǎn)生讀寫錯(cuò)誤滤愕。
運(yùn)行app,先進(jìn)入到待辦事項(xiàng)界面怜校,然后通過Shift+Command+H间影,回到主界面,然后點(diǎn)擊Stop中斷app茄茁。
小貼士:如果你不先回到iOS主界面魂贬,而是通過Xcode直接殺死app,那么你做的改動(dòng)不會(huì)被保存裙顽,這一點(diǎn)和真實(shí)的設(shè)備不一樣付燥。
??:如果你的待辦事項(xiàng)為空,那么此時(shí)app會(huì)掛掉愈犹,這是我們下一節(jié)課要解決的問題机蔗。你可以先把viewDidAppear()注釋掉,先增加幾個(gè)待辦事項(xiàng)進(jìn)去甘萧,然后再把注釋去掉重新運(yùn)行萝嘁,或者什么都不要做,等我們下節(jié)課解決完問題后再說扬卷。