這是***【總結(jié)回顧】iOS Apprentice Tutorial 2:Checklists ***系列的第五篇文章岸梨,前幾篇文章請(qǐng)見(一) 抒蚜、(二)涮总、(三)寻歧、(四)掌栅。
本篇文章總結(jié)本書的第七、八章( Saving and loading the checklist items码泛、Multiple checklists)中的重點(diǎn)內(nèi)容猾封,從126頁(yè)到172頁(yè)。第七章以數(shù)據(jù)持久化的內(nèi)容為主噪珊,第八章主要是增加了一個(gè)嵌套清單晌缘,之前已經(jīng)學(xué)過(guò)如何創(chuàng)建 table view controller,作者盡可能地用另外一種方法實(shí)現(xiàn)同樣的效果痢站。
50. 數(shù)據(jù)持久化三件事
1) 找到可以存放文件的路徑磷箕,創(chuàng)建文件。
找到路徑首頁(yè)要了解一下iOS的沙盒機(jī)制阵难,每個(gè)App都有自己的文件目錄岳枷,不能進(jìn)入其他App的文件目錄里。沙盒機(jī)制能夠保護(hù)手機(jī)不受手機(jī)病毒的干擾呜叫。
所以空繁,App可以存儲(chǔ)數(shù)據(jù)的文件目錄名字為“Document”,Document里的內(nèi)容會(huì)和iTunes或iCloud同步朱庆。當(dāng)發(fā)布新的版本后盛泡,Document里的內(nèi)容仍然在。App目錄里除了Document之外還有其他的文件夾娱颊?有傲诵,Library和tmp兩個(gè)文件夾凯砍。Library里都是是cache文件,和偏好設(shè)置文件掰吕。Library是由系統(tǒng)控制管理的果覆。tmp文件夾里都是臨時(shí)文件反症,tmp里的文件都會(huì)時(shí)不時(shí)地被系統(tǒng)清理刪除垒迂。
所以,我們就把數(shù)據(jù)存儲(chǔ)到Document里凰慈。
那么菱属,接下來(lái)需要的做2件事情:找路徑钳榨、創(chuàng)建存儲(chǔ)文件
//找路徑
func documentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
return paths[0]
}
//在找到的路徑里創(chuàng)建文件
func dataFilePath() -> String {
return(documentsDirectory() as NSString).stringByAppendingPathComponent("某某.plist")
}
注意:.DocumentDirectory
和DocumentationDirectory
的區(qū)別。
我們創(chuàng)建的文件的擴(kuò)展名為.plist
纽门,plist表示Property List 薛耻,是XML文件格式,能夠存儲(chǔ)結(jié)構(gòu)化數(shù)據(jù)赏陵。
2)把 數(shù)據(jù) 存放到文件中饼齿,每當(dāng)用戶改變了數(shù)據(jù)時(shí),改變后的數(shù)據(jù)也能同步存放到數(shù)據(jù)中蝙搔。
我們保存數(shù)據(jù)需要用到 NSCoder缕溉,可以將數(shù)據(jù)儲(chǔ)到結(jié)構(gòu)化格式文件里。將對(duì)象轉(zhuǎn)換成文件吃型,再將文件轉(zhuǎn)換回來(lái)的過(guò)程证鸥,就是 Serialization(序列化)。
func saveChecklists() {
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
archiver.encodeObject(lists, forKey: "Checklists")
archiver.finishEncoding()
data.writeToFile(dataFilePath(), atomically: true)
}
方法saveChecklists()
用了兩步將 items 數(shù)組轉(zhuǎn)換成了二進(jìn)制數(shù)據(jù):
a. NSKeyedArchiver
能將數(shù)組和 ChecklistItem 轉(zhuǎn)換成二進(jìn)制文件然后寫入對(duì)應(yīng)的文件里勤晚。
b. data
放置在NSMutableData
對(duì)象里枉层,然后將自己寫入文件所在的路徑中
最后一點(diǎn),NSKeyedArchiver
知道如何encode一個(gè)數(shù)組對(duì)象赐写,但是并不了解 ChecklistItem鸟蜡,所以,需要讓 ChecklistItem 遵守 NSCoding 協(xié)議才可以挺邀。也就是說(shuō)揉忘,凡是NSKeyedArchiver
要encode的對(duì)象,都要遵守 NSCoding 協(xié)議悠夯。或者說(shuō)躺坟,你想讓某個(gè)對(duì)象使用 NSCoder 系統(tǒng)沦补,就要讓這個(gè)對(duì)象遵守 NSCoding 協(xié)議。有關(guān) NSCoding 的知識(shí)點(diǎn)請(qǐng)見 #53咪橙。
3)應(yīng)用啟動(dòng)時(shí)能夠加載數(shù)據(jù)(取數(shù)據(jù))夕膀。
func loadChecklists() {
let path = dataFilePath()
if NSFileManager.defaultManager().fileExistsAtPath(path) {
if let data = NSData(contentsOfFile: path) {
let unarchiver = NSKeyedUnarchiver(forReadingWithData: data)
lists = unarchiver.decodeObjectForKey("Checklists") as! [Checklist]
unarchiver.finishDecoding()
}
}
}
51. NSCoding 協(xié)議
協(xié)議里有兩個(gè)方法是必須要實(shí)現(xiàn)的:
-
func encodeWithCoder(aCoder: NSCoder)
用來(lái)saving 或者 encoding 對(duì)象虚倒。(存) -
init?(coder aDecoder: NSCoder)
初始化方法,用于創(chuàng)建新的對(duì)象产舞,通過(guò)從 plist 文件里 loading 或者 decoding 對(duì)象來(lái)創(chuàng)建對(duì)象魂奥。(取)
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "Name")
aCoder.encodeObject(items, forKey: "Items")
}
當(dāng)NSKeyedArchiver
嘗試encodeChecklistItem
對(duì)象時(shí)易猫,NSKeyedArchiver
會(huì)給ChecklistItem
發(fā)送encodeWithCoder(coder)
消息耻煤。
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObjectForKey("Name") as! String
items = aDecoder.decodeObjectForKey("Items") as! [ChecklistItem]
super.init()
}
52. 調(diào)試bug小技巧
有時(shí)候出現(xiàn)bug時(shí),Xcode會(huì)轉(zhuǎn)換到 debugger 情景下准颓,顯示哪一行代碼導(dǎo)致了程序崩潰哈蝇。不過(guò)有時(shí)候會(huì)顯示是 AppDelegate 的問(wèn)題,如下圖:
這對(duì)改bug來(lái)說(shuō)可沒(méi)有什么幫助攘已。那怎么辦呢炮赦?見下圖:
Breakpoint navigator -> 點(diǎn)擊 +
然后再次 Run,Xcode就會(huì)顯示真正導(dǎo)致crash的代碼行了样勃。
53.題外話
- 作者推薦了一個(gè)Mac軟件:TextWrangler吠勘。
- 在Xcode里如果遇到看不懂的方法,按住Alt/Option鍵峡眶,點(diǎn)擊這個(gè)代碼即可出現(xiàn)幫助信息剧防。
- 幫你區(qū)分兩個(gè)文件中代碼異同的小工具:Xcode -> Open Developer Tool -> FileMerge
54. Initializers 構(gòu)造器
在創(chuàng)建新的對(duì)象時(shí),才需要 init 方法幌陕。比如:當(dāng)用戶點(diǎn)擊+時(shí)诵姜,用init()
來(lái)創(chuàng)建 ChecklistItem,用init?(coder)
將 ChecklistItems存儲(chǔ)到硬盤上搏熄。
寫 init()
的標(biāo)準(zhǔn)步驟:
init() {
//給常量或變量實(shí)例賦值
super.init()
//其他初始化代碼棚唆,比如調(diào)用方法,寫在這里就好心例。必須在super.init()之后宵凌,不然報(bào)錯(cuò)
}
init 方法不用 func 關(guān)鍵詞開頭。
override init
和 required init?
止后, 一個(gè)對(duì)象A是對(duì)象B的子類瞎惫,如果要在對(duì)象A里添加init方法,前面需要有 override或者required译株,比如:
init(name: String) {
self.name = name
super.init()
}
override init() {
super.init()
}
當(dāng)init有問(wèn)號(hào)時(shí)瓜喇,表示當(dāng) init 失敗時(shí),會(huì)返回nil歉糜。如果plist文件里沒(méi)有足夠的信息乘寒,decoding一個(gè)對(duì)象就會(huì)失敗。
當(dāng)你聲明一個(gè)變量或者常量時(shí)匪补,需要給常量或變量一個(gè)初始值伞辛。
如果聲明了變量烂翰,卻沒(méi)有給出初始值,只給出了類型蚤氏,比如:
var checked: Bool
這樣的話甘耿,必須要在init方法里給變量賦值。不然竿滨,Swift會(huì)報(bào)錯(cuò)(Optional 類型的變量除外)佳恬。
在給所有的變量常量實(shí)例都賦值后,就可以調(diào)用 super.init()
方法來(lái)初始化這個(gè)對(duì)象的superclass(父類)姐呐。之后殿怜,就可以寫其他初始化代碼,比如調(diào)用某些方法曙砂,必須在super.init()之后头谜,不然報(bào)錯(cuò)。
雖然 Swift 的初始化規(guī)則看起來(lái)比較復(fù)雜鸠澈,還好柱告,要你你忘了提供init方法,編譯器會(huì)提示你的笑陈。
最后以 table view controller 舉例际度, table view controller 和很多其他的對(duì)象一樣,會(huì)有多個(gè) init 方法:
-
init?(coder)
:view controller 自動(dòng)從 storyboard 中載入 -
init(nibName, bundle)
:你想手動(dòng)從一個(gè)nib文件中載入 view controller -
init(style)
:你想不使用 storyboard 或 nib 來(lái)創(chuàng)建 table view controller涵妥。
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
注意到 init?(coder)
的參數(shù)有些奇怪了嗎乖菱,外部標(biāo)簽和內(nèi)部標(biāo)簽和其他的方法不太一樣。coder
標(biāo)簽是方法名字的一部分蓬网,方法參數(shù)是aDecoder
窒所。
當(dāng)年調(diào)用super.init
方法,用coder
標(biāo)簽表示super的初始化方法的參數(shù)帆锋,從aDecoder
來(lái)的對(duì)象作為參數(shù)的值吵取。這句話可能不太好理解,可能是翻譯錯(cuò)了锯厢,附上原文:
When you call
super.init
, you use the label coder to refer to the parameter of super's init method, and the object from aDecoder as that parameter's value.
總結(jié)一下 init 方法三步驟:
1)確保實(shí)例變量有值
2)調(diào)用 superclass 的 init()皮官,
3)調(diào)用其他的方法
55. 創(chuàng)建 table view cell 的四種方法
方法一:使用 prototype cells
在storyboard中找到cell,輸入identifier:ChecklistItem实辑,然后寫代碼:
let cell = tableView.dequeueReusableCellWithIdentifier("ChecklistItem", forIndexPath: indexPath)
注意dequeueReusableCellWithIdentifier
方法里有參數(shù)forIndexPath
捺氢,只能用在 prototype cells 中。
方法二:使用靜態(tài)cell(static cells)
已經(jīng)確定有哪些cell剪撬,而且內(nèi)容不會(huì)變動(dòng)摄乒。
方法三:使用nib文件
nib,也就是XIB,有點(diǎn)像是迷你型的storyboard缺狠,里面包含定制的 UITableViewCell 對(duì)象。
方法四:手動(dòng)創(chuàng)建
let cellIdentifier = "Cell"
if let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
}
注意dequeueReusableCellWithIdentifier
方法里沒(méi)有參數(shù)萍摊。
這樣可能不太深刻挤茄,實(shí)際使用的時(shí)候是什么樣子呢?如下:
func cellForTableView(tableView: UITableView) -> UITableViewCell {
let cellIdentifier = "Cell"
if let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
}
}
override func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
let cell = cellForTableView(tableView)
let checklist = lists[indexPath.row]
cell.textLabel!.text = checklist.name
cell.accessoryType = .DetailDisclosureButton
return cell
}
總之冰木,對(duì)于 UITabieViewCell穷劈,我有一個(gè)忠告:
盡可能的復(fù)用cell(reuse cells)
盡可能的復(fù)用cell(reuse cells)
盡可能的復(fù)用cell(reuse cells)
重要的事情說(shuō)三遍。
56. 新方法之點(diǎn)擊跳轉(zhuǎn)界面的同時(shí)傳值(一)
首先踊沸,storyboard中歇终,黃點(diǎn)拖動(dòng)(見下圖),輸入Identifier:ShowChecklist逼龟。
然后寫代碼:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("ShowChecklist", sender: nil)
}
上面代碼中有 sender评凝,借用sender可以傳值(這個(gè)功能是重點(diǎn),省時(shí)省力好幫手)腺律,如下:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let checklist = lists[indexPath.row]
performSegueWithIdentifier("ShowChecklist", sender: checklist)
}
理解這兩行代碼非常關(guān)鍵奕短。
當(dāng)然,還不能忘了 prepare?ForSegue(sender)
方法匀钧。
override func prepareForSegue(sender: UIStoryboardSegue, sender: AnyObject?) {
if segue.identifier == "ShowChecklist" {
let controller = segue.destinationViewController as! ChecklistViewController {
controller.checklist = sender as! Checklist //看翎碑,用上 sender 了!
}
}
}
當(dāng)然了之斯,ChecklistViewController里一定要聲明(聲明里為什么要有嘆號(hào)日杈,在后面會(huì)提及):
var checklist: Checklist!
好了,上面就是所有的步驟了佑刷。
接下來(lái)說(shuō)一下上述步驟中涉及的一些知識(shí)點(diǎn)莉擒,先看圖,看看實(shí)際上 perform 一個(gè) sugue 涉及多少步驟项乒,然后講解知識(shí)點(diǎn):
- 調(diào)用順序啰劲。
viewDidLoad()
在prepareForSegue()
之后調(diào)用,也就是說(shuō)檀何,先調(diào)用prepareForSegue()
蝇裤,然后再調(diào)用viewDidLoad()
。 - checklist為什么要有嘆號(hào)频鉴。加一個(gè)嘆號(hào)可以允許 checklist 暫時(shí)為 nil 直到
viewDidLoad()
被調(diào)用栓辜。
在#59里,會(huì)介紹另外一種也就是第三種跳轉(zhuǎn)頁(yè)面并且傳值的方法垛孔。
57. 創(chuàng)建自己的構(gòu)造器(init 方法)
var list = Checklist()
list.name = "Name of the checklist"
想把上面的兩行變成下面這一行藕甩,該怎么做呢?
list = Checklist(name: "Name of the checklist")
需要寫一個(gè)自己的 init 方法周荐,讓 name 作為一個(gè)參數(shù):
init(name: String) {
self.name = name
super.init()
}
這個(gè)構(gòu)造器的作用就是把參數(shù) name 賦值給實(shí)例變量(self.name)狭莱。
self.name 指的是當(dāng)前 Checklist 對(duì)象的變量 name僵娃。
創(chuàng)建這個(gè)構(gòu)造器的好久就是,可以保證每次我創(chuàng)建新的 Checklist 對(duì)象時(shí)腋妙,都一定會(huì)有 name 屬性默怨。
58. Type Cast(類型檢查)
類型檢查的目的是讓 Swift 把某個(gè)值擁有不同的數(shù)據(jù)類型。
59. 新方法之點(diǎn)擊跳轉(zhuǎn)界面的同時(shí)傳值(二)
點(diǎn)擊 cell 里的 Accessory骤素,除了用 storyboard 之外匙睹,還可以用:
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
}
點(diǎn)擊 cell 的 Accessory 跳轉(zhuǎn)界面并且傳值的方法如下:
先到 storyboard 中找到你要跳轉(zhuǎn)的目的地界面,然后如下圖济竹;
在 Storyboard ID 中輸入對(duì)應(yīng)的 Identity痕檬,然后寫代碼:
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
let navigationController = storyboard!.instantiateViewControllerWithIdentifier("ListDetailNavigationController") as! UINavigationController
let controller = navigationController.topViewController as! ListDetailViewController
controller.delegate = self
let checklist = dataModel.lists[indexPath.row]
controller.checklistToEdit = checklist
presentViewController(navigationController, animated: true, completion: nil)
}
其中關(guān)鍵代碼兩行:
let navigationController = storyboard!.instantiateViewControllerWithIdentifier("ListDetailNavigationController") as! UINavigationController
presentViewController(navigationController, animated: true, completion: nil)
這個(gè)方法非常好用~