iOS Apprentice中文版-从0开始学iOS开发-第十九课

保存待办事项

在本小节,你将会写代码来保存用户的新增、编辑、或者删除的内容到Checklists.plist文件。并且你保存到这些内容会在app重新启动后被读取回来。

那么什么是.plist文件呢?

你已经在Bull's Eye这个课程中见过了Info.plist文件。所有的app都有这个文件,包括我们现在的这个app,你可以在工程导航器中看到这个文件。Info.plist包含app的各种属性,可以给iOS系统更多的关于这个app的信息,比如app的展现名称。

“plist”是Property List的简写,是一种XML文件,用于格式化数据结构,通常以许多设置及设置的值的形式存在。Property List文件在iOS中使用非常频繁。它适用于多种数据类型的存储,并且是最简单易用的一个。我们都爱它!

为了存储checklist items你必须要使用NSCoder系统,NSCoder系统可以使对象存储它们的数据到一个格式化的文件中。

你无需关心太多格式化的事。所有你要关心的事就是数据会存储在app的Documents文件夹下的某种文件中,其余的技术细节都可以交给NSCoder去处理。

你已经在多种场景中使用过NSCoder了,其实这就是故事模版的工作方式。当你在故事模版中添加一个新的view controller时,Xcode使用NSCoder系统将这个对象写入文件,这一步叫做(encoding,编码),然后当你的应用启动时,它又使用NSCoder来从故事模版中读取这个对象,这一步叫做(decoding,解码)。

将对象写入到文件然后在读取出来的这个过程叫做序列化(srialization)。这是软件工程中的一个大课题。

其实这个过程有点像我们使用电冰箱的过程,你先把新鲜蔬菜(对象)放到冰箱的某一个抽屉里里冻起来(相当于iPhone闪存中的某个文件),然后等你要吃的时候再拿出来解冻。(我只想说这个比喻有点好不恰当,不过算了,忠实于原著)

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第1张图片
存储和读取对象的过程

打开ChecklistViewController.swift添加下面这个方法进去:

func saveChecklistItems() {
        let data = NSMutableData()
        let archiver = NSKeyedArchiver(forWritingWith: data)
        archiver.encode(items, forkey: "ChecklistItems")
        archiver.finishEncoding()
        data.write(to: dataFilePath(), atomically: true)
    }

这个方法首先拿到items数组的数据,然后通过两个步骤将items数组转换为二进制数据块,并且将这个二进制数据块写到文件中。

1、NSKeyedArchiver,NSCoder创建plist文件的形式,将数组编码(encode)并且将其中的所有ChecklistItem转换为二进制格式,这样就可以将这些数据写入文件了。

2、数据被放置在一个NSMutableData对象中,然后将自己写入dataFilePath()路径指定的文件中。

目前你不理解NSKeyedArchiver内部的工作原理并不重要。你只需要记住这一行为可以使你将你的对象放入一个文件,并且之后可以读取出来。

你需要在任何items有变动的地方调用saveChecklistItems()这个方法。

练习:你都应该在哪些地方调用这个方法呢?

答案:看看items数组在哪些地方有修改的操作。它们都发生在ItemDetailViewControllerDelegate委托方法中。

还是在ChecklistViewController.swift文件中,在以下方法内部添加saveChecklistItems():

func itemDetailViewController(_ controller: ItemDetailViewController,
...
  saveChecklistItems()
}
func itemDetailViewController(_ controller: ItemDetailViewController,
...
  saveChecklistItems()
}

不要忘了扫动删除行的地方:

override func tableView(_ tableView: UITableView,
                       commit editingStyle: UITableViewCellEditingStyle,
...
  saveChecklistItems()
}

还有开关对勾符号的地方:

override func tableView(_ tableView: UITableView,
...
  saveChecklistItems()
}

仅仅是对items数组调用NSKeyedArchiver是不够的。如果你现在运行app,做些什么,比如说通过点击将某一行的对勾符号去掉,app会立马死给你看。不信可以试试:

*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason: '-[Checklists.ChecklistItem
encodeWithCoder:]: unrecognized selector sent to instance 0x7f8d6af3aac0

上面的报错中提到了selector(选择器)这样一个东西,selector是Object-C中的术语,用于方法的名称,所以这个报错的意思是app试图调用一个叫做encodeWithCoder()的方法,但是没有找到它。(Swift的术语中不存在selector,但是因为iOS的框架是由Object-C写成的,所以比较底层的报错中你会看到Object-C的术语)

报错的同时Xcode的窗口会切换到调试器,并且指出是哪里引发了问题,你得到的提示大约会是下面这个样子:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第2张图片
提示不是非常明确

调试区域将报错的源头指向AppDelegate.swift源文件,这是一个误导。这时我们需要用一种叫做断点的工具来调试:

切换到断点导航器(见下图),并且点击底部的➕号按钮:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第3张图片
添加断点

再运行一次app试试。

注意:如果你的app运行时立马崩溃了,那么就是UIKit底层的异常被触发了。这不是由于你的代码造成的。异常断点的作用就是捕捉任何异常,即使并不是非常致命的异常或者是由于系统导致的异常。在这种情况下,你可以点击几次following按钮(见下图)来跟踪一下:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第4张图片
following按钮

这个按钮的作用是继续运行程序,你可以在Xcode底部找到它。

点击app上的某行上的对勾符号。这一次Xocde就会定位到正确的错误源头了,它是在saveChecklistItems()中:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第5张图片
罪魁祸首在这里

罪犯现形:

archiver.encode(items, forKey: "ChecklistItems")

显然app是在试图对items数组编码时崩溃掉的,或者是数组内部的数据出了问题。

这个“unrecognized selector”报错信息意味着你忘记了执行某个方法。很显然这个被遗忘的方法和对ChecklistItem进行encode(with)有关。

真相是这样的:你指使NSKeyedArchiver对items数组进行编码,所以在编码时,程序不仅要对数组进行编码,而且要对数组内的每一个ChecklistItem对象进行编码,而NSKeyedArchiver目前对ChecklistItem一无所知。所以你必须帮它一把。

在ChecklistItem.swift中添加一个NSCoding,就在下面这一行添加:

class ChecklistItem: NSObject,NSCoding {

添加完以后,轮到编译器死给你看了,它说ChecklistItem没有遵守NSCoding协议。

这里的名字有点绕:如果你想要在你的对象上使用NSCoder系统的话,你就必须执行NSCoding协议。

之前我们介绍过,协议就是一组方法名称的列表。遵守一个协议就意味着你需要执行协议中的方法。

NSCoding协议中的方法是:

func encode(with aCoder: NSCoder)

init?(coder aDecoder: NSCoder)

不算太坏,只有两个方法而已。第一个是用于存储或者说用于编码的方法。第二个是一种特殊的init方法。回忆一下init方法用于创建一个新的对象。这里的init方法用于创建从plist文件中读取出来的对象。

在ChecklistItem.swift中添加以下方法:

func encode(with aCoder: NSCoder) {
        aCoder.encode(text, forKey: "Text")
        aCoder.encode(checked, forKey: "Checked")
    }

这就是被遗忘的那个方法。

当NSKeyedArchiver试图对ChecklistItem对象编码时,它会先给Checklist item发送一条encode(with)消息。

这个方法翻译成白话文就是:一个ChecklistItem需要保存一个名叫Text的对象,这个对象包含实例变量text的值,以及一个名为Checked的对象,这个对象包含变量checked的值。

添加这个方法足以让coder系统工作了,至少可以用于保存对象了。

在你能正常运行app前,你还需要添加点东西。Swift需要你执行协议中的所有方法,而NSCoding协议中有两个方法,一个用于保存,另一个用于读取。

在ChecklistItem.swift中添加第二个方法:

required init?(coder aDecoder: NSCoder) {
        super.init()
    }

现在我们还没有实际的用到它,仅仅是为了编译器不报错,简单的添加了这个方法。

⚠️:你还记得init?(coder)吗,我们之前使用过的,用于初始化ChecklistViewController。因为故事模版也需要使用NSCoding来读取对象到app中。

init是Swift中比较特殊的一个方法,因为你添加了init?(coder)所以你必须同时添加没有参数的init()方法。如果不这样做,还是无法通过编译。我们后面会学习更多关于这个的知识。

在ChecklistItem.swift中添加:

override init() {
        super.init()
    }

这个方法没有任何作用,仅仅保证顺利通过编译。

运行app,然后点击某行上的对勾符号试试,看能不能正常工作。

回到Finder窗口中,找到app的Document目录:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第6张图片
Checklists.plist文件已经生成了

现在你可以看到Checklists.plist文件了,它包含table view中的待办项目的数据。

你可以打开这个文件看看,但是可能里面的内容会让人头晕眼花,虽然它是XML格式,但不是用来给人看的那种,只有NSKeyedArchiver系统能看懂。

也许你看到的不是XML格式的东西,而是二进制的格式,这是因为有些文本编辑器不支持自动转换格式,你可以从Mac store下载一个TextWrangler试试,非常不错。

你也可以利用Finder的快速预览功能来查看这个文件,选中这个文件然后点击空格键就可以了。

你也可以右键选择打开方式为Xcode。

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第7张图片
在Xcode中打开plist文件

还是有点看不太懂,不过多了解一下总是好的。

你可以展开几行看看,里面即包括NSKeyedArchiver也有ChecklistItem,至于它们是如何自洽的组合在一起的,我并不关心。

NS对象

以NS为前缀开头的对象都是由Foundation框架提供的。NS是NextStep的缩写,从1990年开始NS对象就是Mac OS和iOS的基础部分。

如果你困惑于像NSKeyedArchiver和NSCoding这类对象到底是如何工作的,你可以把鼠标移到它们上面然后使用Option键查看它们的介绍。

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第8张图片

我总是提醒自己如何利用框架中提供的对象以及方法。

对框架中可用的对象有个大体的把握是很好的,但是没有人可以把一个框架内的方方面面都背下来。所以养成查看文档的习惯是非常有必要的,如果你遇到了一个新的对象的话。这样你会非常快的熟悉iOS框架。

读取文件

存储功能已经做好了,但是只是存储是没有用的,我们还需要对Checklists.plist做读取操作。这非常简单,和保存的过程一样,只是动作是相反的。

我们之前在ChecklistItem.swift文件中添加了一个空的init?(coder)方法,现在是时候添加点东西进去了。

将init?(coder)修改为下面这个样子:

required init?(coder aDecoder: NSCoder) {
        text = aDecoder.decodeObject(forKey: "Text") as! String
        checked = aDecoder.decodeBool(forKey: "Checked")
        super.init()
    }

在init?(coder)你做了于encode(with)相反的操作。你从NSCoder的解码对象中取得了对象并且将它们放回到原来的变量中。

你之前在存储在Text中的内容现在重新回到了text变量中。checked也是同样道理。需要值得注意的是对text操作时用的是decodeObject,对checked操作时用的是decodeBool。不同的数据类型要用不同的解码器。

初始化

在Swift中名为init的方法是一种特殊的方法。它们仅在你创建新的对象时使用,为我们创建好这些新的对象随时可以使用。

把这件事想象为买一件新衣服。买来以后这件衣服就是你的私人财产了(相当于为对象分配好了一块内存),但是这件衣服仍然在手提袋里,你要把它穿到身上,才算使用了这件衣服。

但你用下面的语句创建一个新的对象时:

let item = ChecklistItem()

swift首先会为这个新的对象分配足够的存储空间然后调用ChecklistItme的init()方法,没有参数的这一个。

通过NSKeyedUnarchiver对象完成Checklists.plist文件的读取时,会先创建ChecklistItem对象,然后进行解压:

let item = ChecklistItem(coder: someDecoderObject)

这个操作同样也为新的ChecklistItem分配了内存,但是它调用的是init?(coder)而不是init()。

一个对象有多个init方法是非常常见的,具体使用那一个要看具体情况。

当用户点击➕号按钮创建ChecklistItem对象时,你使用init(),当读取存储中的ChecklistItem对象时,你使用init?(coder)。

无论你是调用init()还是init?(coder),这些init方法的实现都是遵循一些列步骤的,哪怕是以后你自己写的init方法,也要遵循这些步骤。

这是最基础的init方法调用:

init() {
  // Put values into your instance variables and constants.
  super.init()
  // Other initialization code, such as calling methods, goes here.
}

注意一下init方法和其他方法不同,没有func关键字。

有时你会看到override init 或者 required init?。当你为其他对象的子类的对象添加init方法时,必须这样做。更多的我们后面讲。

当init可能会返回失败或者返回nil时,就需要加上一个问号。你可以理解为当为一个对象解码时,plist文件中的数据丢失了,不够全,导致解码失败。

在init方法的内部,你首先要确保所有的实例变量或者常量都有值。回忆一下,swift要求所有的变量或者常量都必须有值,除了可选型。

当你声明一个实例变量时你可以给它一个初始值,比如:

var checked = false

有时候会只写变量名称和类型,但是没有初始值,比如:

var checked: Bool

这种情况下,在init方法内部一定要先给它赋值,比如:

init() {
  checked = false
  super.init()
}

如果你不这样做的话,swift会认为这里有致命错误,唯一的特例就是可选型,它们不一定必须有值。

当你确定所有的变量或者常量都有值了以后,你调用super.init()来初始化这个对象的父类。如果你以前没有接触过面向对象编程的话,你可能不知道什么是父类,没关系,忽视它吧,我们会在下个课程中详细讨论这个话题。

你只需要记住有时对象需要向叫做super的东西发送消息,如果你忘记了,app就会死给你看。

调用super.init()后,你可以进行额外的初始化配置,比如调用这个对象自己的方法。但是在你调用super.init()前不能这样做,因为此时swift不能确保你的对象的变量都有值。

你不需要总是写init方法,如果你的init方法本身不需要任何东西的话,没有任何的变量需要处理,那么你可以忘掉init,编译器会自动为你提供一个。

当你最开始创建ChecklistItem时,并没有用到init()方法,但是后面我们用到了init?(coder),所以此时init()就不能省略了,你必须写出来。

swift的初始化规则有些复杂,但是幸运的是,当你忘记它时,编译器会提醒你的。

ChecklistItem的实现就全部完成了。它可以把plist文件中已经序列化的数据读取会app中,但是你还要写一些代码才能正确的显示读取回来的结果。这次是要改动ChecklistViewController。

table view对象,和其他对象一样,有超过一个的init方法,它们是:

init?(coder)用于从故事模版中自动加载视图控制器。

init(nibName,bundle)用于你手工从视图控制器中加载nib(nib文件和故事模版一样,只是仅包含一个视图控制器)

init(style)用于你想要创建没有故事模版或者nib时的table view controller。

这些视图控制器是来自故事模版的,所以你要将plist文件中读取到的东西放进init?(coder)里。这和你在实现ChecklistItem时用到的差不多。

UITableViewController通过故事模版从同样的NSCoder系统中由你自己创建的plist文件中被读取出来并且解码。

打开ChecklistViewController.swift,将init?(coder)修改为:

required init?(coder aDecoder: NSCoder) {
  items = [ChecklistItem]()
  super.init(coder: aDecoder)
  loadChecklistItems()
}

这次在这个init方法中我们做了三件事:

1、你确保实例变量items一定是有值的。

2、然后你调用了父级的init()。这一次调用的是super.init(coder),确保这个视图控制器的剩余部分可以正常的从故事模版中解码。

3、最后,你可以调用一些其他方法。这里你调用了一个新的方法,这个新的方法的作用就是从plist文件中读取数据。

⚠️:你注意到了吗?这个init?(coder)有不同的外部和内部名称。coder是方法名称的一部分,但是在方法的内部还有一个参数叫做aDecoder。
当你调用super.init时,你使用名称coder来引用super.init的参数,而aDecoder中的对象就是这个参数的值。

接下来我们来添加loadChecklistItems()方法:

func loadChecklistItems() {
        //1
        let path = dataFilePath()
        //2
        if let data = try?Data(contentsOf: path) {
            //3
            let unarchiver = NSKeyedUnarchiver(forReadingWith: data)
            items = unarchiver.decodeObject(forKey: "ChecklistItems") as! [ChecklistItem]
            unarchiver.finishDecoding()
        }
    }

一步步来解释:

1、首先你将dataFilePath()的结果放倒了一个名叫path的临时常量里。

2、试着从Checklists.plist中读取内容到一个新的数据对象中。关键字Try?的意思是就是字面意思,试试,如果有数据对象就读出来,如果是nil就不读了。这就是为什么我们用if let解包的原因,因为它可能为nil。
为什么可能会失败呢?假如根本没有Checklists.plist文件的话,显然也不会有什么ChecklistItem对象。这一情况会在app初次运行时发生。在这种情况下,你需要跳过这个方法的其余部分。

3、当app找到Checklists.plist文件,你要从文件中把整个数组读取出来。这一行为和saveChecklistItems()截然相反。

你创建了一个NSKeyedUnarchiver对象,就是unarchiver,并且要求它将数据解码到items数组中。这一步将文件中曾经未解码的ChecklistItem对象的拷贝填充到数组中。

⚠️:检查一下,loadChecklistItem()和saveChecklistItems()两个方法中都是使用“ChecklistItems”这个关键字来进行解码和编码的。如果你输入错了,那么app就会死给你看。
通常Xcode的智商都足以为你指出错误,但是没有聪明到能指出这种错误。

运行app,并且做些操作。然后点击Stop按钮中断app,再重新运行一次,你会发现你第一次到操作结果都保存了下来。

在此点击Stop按钮中断app,然后打开Finder,找到Documents文件夹并且删除掉Checklists.plist文件,再运行一次app,你会发现所有的东西都消失了。

然后再添加一条待办记录,你会发现Checklists.plist又重新生成了。

棒极了,你写了一个app不仅可以添加、编辑数据,并且还可以持久化的保存数据,这些都是app开发的基本功。以及使用委托传送数据都是iOS开发的基础技能。

使用FileMerge来比较文件

你可以对你写的app代码和我的进行对比,通过使用叫做FileMerge的工具。它位于Xcode的菜单Xcode->Open Developer Tool->FileMerge中:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第9张图片

然后分别选择两个文件的位置:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第10张图片

等一段时间后,FileMerge会告诉你两个文件的差异在哪里:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第11张图片

双击列表中的文件名,就可以查看两个文件的详细内容了:

iOS Apprentice中文版-从0开始学iOS开发-第十九课_第12张图片

FileMerge是非常有用的一个工具,用于定位两个文件之间的差异,我经常使用它。

如果你在写代码的过程中,没有达到理想的效果,就可以通过这种方法和我的对比一下。

到了可以休息一会的时间了,停下来泡泡脚,然后畅想一下未来你自己的app位居榜首的情景。

如果你有不理解的地方,那么回头重新阅读一遍是不二的选择。不要冲的太快,我并没有什么奖牌可以给你。相比之下你还不如花时间慢慢的把这些东西消化为自己的,这才是你的奖牌。

和往常一样,自己动手去改改代码,添加一些你想要的新功能,我们鼓励你把app搞到不能运行为止,这就是学习的方法。

确保你消化了到目前为止的所有内容,接下来我们会为这个app添加一些新的功能,也会重复之前的内容,相当于复习了。

但是眼下,你还是先停下来,休息一下,调整调整自己。

你可能感兴趣的:(iOS Apprentice中文版-从0开始学iOS开发-第十九课)