这是***【总结回顾】iOS Apprentice Tutorial 2:Checklists ***系列的第五篇文章,前几篇文章请见(一) 、(二)、(三)、(四)。
本篇文章总结本书的第七、八章( Saving and loading the checklist items、Multiple checklists)中的重点内容,从126页到172页。第七章以数据持久化的内容为主,第八章主要是增加了一个嵌套清单,之前已经学过如何创建 table view controller,作者尽可能地用另外一种方法实现同样的效果。
50. 数据持久化三件事
1) 找到可以存放文件的路径,创建文件。
找到路径首页要了解一下iOS的沙盒机制,每个App都有自己的文件目录,不能进入其他App的文件目录里。沙盒机制能够保护手机不受手机病毒的干扰。
所以,App可以存储数据的文件目录名字为“Document”,Document里的内容会和iTunes或iCloud同步。当发布新的版本后,Document里的内容仍然在。App目录里除了Document之外还有其他的文件夹?有,Library和tmp两个文件夹。Library里都是是cache文件,和偏好设置文件。Library是由系统控制管理的。tmp文件夹里都是临时文件,tmp里的文件都会时不时地被系统清理删除。
所以,我们就把数据存储到Document里。
那么,接下来需要的做2件事情:找路径、创建存储文件
//找路径
func documentsDirectory() -> String {
let paths = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)
return paths[0]
}
//在找到的路径里创建文件
func dataFilePath() -> String {
return(documentsDirectory() as NSString).stringByAppendingPathComponent("某某.plist")
}
注意:.DocumentDirectory
和DocumentationDirectory
的区别。
我们创建的文件的扩展名为.plist
,plist表示Property List ,是XML文件格式,能够存储结构化数据。
2)把 数据 存放到文件中,每当用户改变了数据时,改变后的数据也能同步存放到数据中。
我们保存数据需要用到 NSCoder,可以将数据储到结构化格式文件里。将对象转换成文件,再将文件转换回来的过程,就是 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 数组转换成了二进制数据:
a. NSKeyedArchiver
能将数组和 ChecklistItem 转换成二进制文件然后写入对应的文件里。
b. data
放置在NSMutableData
对象里,然后将自己写入文件所在的路径中
最后一点,NSKeyedArchiver
知道如何encode一个数组对象,但是并不了解 ChecklistItem,所以,需要让 ChecklistItem 遵守 NSCoding 协议才可以。也就是说,凡是NSKeyedArchiver
要encode的对象,都要遵守 NSCoding 协议。或者说,你想让某个对象使用 NSCoder 系统,就要让这个对象遵守 NSCoding 协议。有关 NSCoding 的知识点请见 #53。
3)应用启动时能够加载数据(取数据)。
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 协议
协议里有两个方法是必须要实现的:
-
func encodeWithCoder(aCoder: NSCoder)
用来saving 或者 encoding 对象。(存) -
init?(coder aDecoder: NSCoder)
初始化方法,用于创建新的对象,通过从 plist 文件里 loading 或者 decoding 对象来创建对象。(取)
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: "Name")
aCoder.encodeObject(items, forKey: "Items")
}
当NSKeyedArchiver
尝试encodeChecklistItem
对象时,NSKeyedArchiver
会给ChecklistItem
发送encodeWithCoder(coder)
消息。
required init?(coder aDecoder: NSCoder) {
name = aDecoder.decodeObjectForKey("Name") as! String
items = aDecoder.decodeObjectForKey("Items") as! [ChecklistItem]
super.init()
}
52. 调试bug小技巧
有时候出现bug时,Xcode会转换到 debugger 情景下,显示哪一行代码导致了程序崩溃。不过有时候会显示是 AppDelegate 的问题,如下图:
这对改bug来说可没有什么帮助。那怎么办呢?见下图:
Breakpoint navigator -> 点击 +
然后再次 Run,Xcode就会显示真正导致crash的代码行了。
53.题外话
- 作者推荐了一个Mac软件:TextWrangler。
- 在Xcode里如果遇到看不懂的方法,按住Alt/Option键,点击这个代码即可出现帮助信息。
- 帮你区分两个文件中代码异同的小工具:Xcode -> Open Developer Tool -> FileMerge
54. Initializers 构造器
在创建新的对象时,才需要 init 方法。比如:当用户点击+时,用init()
来创建 ChecklistItem,用init?(coder)
将 ChecklistItems存储到硬盘上。
写 init()
的标准步骤:
init() {
//给常量或变量实例赋值
super.init()
//其他初始化代码,比如调用方法,写在这里就好。必须在super.init()之后,不然报错
}
init 方法不用 func 关键词开头。
override init
和 required init?
, 一个对象A是对象B的子类,如果要在对象A里添加init方法,前面需要有 override或者required,比如:
init(name: String) {
self.name = name
super.init()
}
override init() {
super.init()
}
当init有问号时,表示当 init 失败时,会返回nil。如果plist文件里没有足够的信息,decoding一个对象就会失败。
当你声明一个变量或者常量时,需要给常量或变量一个初始值。
如果声明了变量,却没有给出初始值,只给出了类型,比如:
var checked: Bool
这样的话,必须要在init方法里给变量赋值。不然,Swift会报错(Optional 类型的变量除外)。
在给所有的变量常量实例都赋值后,就可以调用 super.init()
方法来初始化这个对象的superclass(父类)。之后,就可以写其他初始化代码,比如调用某些方法,必须在super.init()之后,不然报错。
虽然 Swift 的初始化规则看起来比较复杂,还好,要你你忘了提供init方法,编译器会提示你的。
最后以 table view controller 举例, table view controller 和很多其他的对象一样,会有多个 init 方法:
-
init?(coder)
:view controller 自动从 storyboard 中载入 -
init(nibName, bundle)
:你想手动从一个nib文件中载入 view controller -
init(style)
:你想不使用 storyboard 或 nib 来创建 table view controller。
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
注意到 init?(coder)
的参数有些奇怪了吗,外部标签和内部标签和其他的方法不太一样。coder
标签是方法名字的一部分,方法参数是aDecoder
。
当年调用super.init
方法,用coder
标签表示super的初始化方法的参数,从aDecoder
来的对象作为参数的值。这句话可能不太好理解,可能是翻译错了,附上原文:
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.
总结一下 init 方法三步骤:
1)确保实例变量有值
2)调用 superclass 的 init(),
3)调用其他的方法
55. 创建 table view cell 的四种方法
方法一:使用 prototype cells
在storyboard中找到cell,输入identifier:ChecklistItem,然后写代码:
let cell = tableView.dequeueReusableCellWithIdentifier("ChecklistItem", forIndexPath: indexPath)
注意dequeueReusableCellWithIdentifier
方法里有参数forIndexPath
,只能用在 prototype cells 中。
方法二:使用静态cell(static cells)
已经确定有哪些cell,而且内容不会变动。
方法三:使用nib文件
nib,也就是XIB,有点像是迷你型的storyboard,里面包含定制的 UITableViewCell 对象。
方法四:手动创建
let cellIdentifier = "Cell"
if let cell = tableView.dequeueReusableCellWithIdentifier(cellIdentifier) {
return cell
} else {
return UITableViewCell(style: .Default, reuseIdentifier: cellIdentifier)
}
注意dequeueReusableCellWithIdentifier
方法里没有参数。
这样可能不太深刻,实际使用的时候是什么样子呢?如下:
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
}
总之,对于 UITabieViewCell,我有一个忠告:
尽可能的复用cell(reuse cells)
尽可能的复用cell(reuse cells)
尽可能的复用cell(reuse cells)
重要的事情说三遍。
56. 新方法之点击跳转界面的同时传值(一)
首先,storyboard中,黄点拖动(见下图),输入Identifier:ShowChecklist。
然后写代码:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
performSegueWithIdentifier("ShowChecklist", sender: nil)
}
上面代码中有 sender,借用sender可以传值(这个功能是重点,省时省力好帮手),如下:
override func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
let checklist = lists[indexPath.row]
performSegueWithIdentifier("ShowChecklist", sender: checklist)
}
理解这两行代码非常关键。
当然,还不能忘了 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 了!
}
}
}
当然了,ChecklistViewController里一定要声明(声明里为什么要有叹号,在后面会提及):
var checklist: Checklist!
好了,上面就是所有的步骤了。
接下来说一下上述步骤中涉及的一些知识点,先看图,看看实际上 perform 一个 sugue 涉及多少步骤,然后讲解知识点:
- 调用顺序。
viewDidLoad()
在prepareForSegue()
之后调用,也就是说,先调用prepareForSegue()
,然后再调用viewDidLoad()
。 - checklist为什么要有叹号。加一个叹号可以允许 checklist 暂时为 nil 直到
viewDidLoad()
被调用。
在#59里,会介绍另外一种也就是第三种跳转页面并且传值的方法。
57. 创建自己的构造器(init 方法)
var list = Checklist()
list.name = "Name of the checklist"
想把上面的两行变成下面这一行,该怎么做呢?
list = Checklist(name: "Name of the checklist")
需要写一个自己的 init 方法,让 name 作为一个参数:
init(name: String) {
self.name = name
super.init()
}
这个构造器的作用就是把参数 name 赋值给实例变量(self.name)。
self.name 指的是当前 Checklist 对象的变量 name。
创建这个构造器的好久就是,可以保证每次我创建新的 Checklist 对象时,都一定会有 name 属性。
58. Type Cast(类型检查)
类型检查的目的是让 Swift 把某个值拥有不同的数据类型。
59. 新方法之点击跳转界面的同时传值(二)
点击 cell 里的 Accessory,除了用 storyboard 之外,还可以用:
override func tableView(tableView: UITableView, accessoryButtonTappedForRowWithIndexPath indexPath: NSIndexPath) {
}
点击 cell 的 Accessory 跳转界面并且传值的方法如下:
先到 storyboard 中找到你要跳转的目的地界面,然后如下图;
在 Storyboard ID 中输入对应的 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)
}
其中关键代码两行:
let navigationController = storyboard!.instantiateViewControllerWithIdentifier("ListDetailNavigationController") as! UINavigationController
presentViewController(navigationController, animated: true, completion: nil)
这个方法非常好用~