每一个iOS app都运行在一个被"隔离"的环境里,这个环境,就是我们经常提及的沙箱。在这个沙箱里,每一个app都认为自己拥有一套独立完整的文件系统可以访问。但实际上,它们只能在自己的沙箱里活动,而对其他app的环境一无所知。
在沙箱里,每一个app都有一个叫做"Documents"的目录,用来保存App的数据。当你通过iTunes或者iCloud备份的时候,每一个App的"Documents"目录都会被备份;而当你更新App的时候,"Documents"目录的内容,也会被完整保留下。总之,无论你想保存App运行中的任何内容,使用"Documents"目录,就对了。
访问Documents目录
首先,我们来看如何访问Documents目录,在EpisodeListViewController里,添加下面的代码:
class EpisodeListViewController: UITableViewController {
// ...
func documentsDirectory() -> NSURL {
let urls = NSFileManager
.defaultManager()
.URLsForDirectory(.DocumentDirectory,
inDomains: .UserDomainMask)
return urls[0]
}
}
首先,我们使用NSFileManager.defaultManager读取了系统默认的文件管理器对象,然后在URLsForDirectory方法里,.DocumentDirectory表示我们要获取Document目录的URL,.UserDomainMask表示在当前用户的Home目录中查找。
尽管URLsForDirectory会返回一个NSURL数组,但是,由于iOS App都运行在自己的沙箱里,因此,Document目录肯定存在并且也只有一个,我们直接返回url[0]就可以了。
当我们要在Document目录里创建文件的时候,我们可以用下面的代码生成一个文件的NSURL:
class EpisodeListViewController: UITableViewController {
// ...
func fileUrl(fileName: String) -> NSURL {
let documentUrl = self.documentsDirectory()
.URLByAppendingPathComponent(fileName)
return documentUrl
}
}
"尽量避免直接使用字符串来拼接路径,使用这些方法会避免很多小错误(例如,你不确定是否需要在文件名前或者URL结尾添加/等)"。
特别提示
然后,我们可以在viewDidLoad方法里,把这些路径打印在控制台上:
class EpisodeListViewController: UITableViewController {
// ...
override func viewDidLoad() {
super.viewDidLoad()
let docUrl = documentsDirectory()
let file = fileUrl("EpisodeList.plist")
print("Document url: \(docUrl)");
print("Data file url: \(file)");
}
}
然后 Command + R编译执行,在控制台里,我们可以看到类似下面的结果:
我们复制URL中路径的部分,然后在Finder里按Command + Shift + G,把路径粘贴进去,打开之后,我们可以看到每一个App都用一个随机字符串来表示,并且,每一个App都有自己的Document / Library / tmp目录。
回到Xcode,接下来,我们来了解什么是plist文件。
什么是plist文件?
所谓plist,就是Property List的缩写,本质上,它是一个XML文件。每一个App都有一个叫做Info.plist的文件,在项目文件列表里,我们选中它,就可以看到类似这样的内容:
其中每一行都表示了一个App的配置信息,如果我们右键选中Info.plist,然后选择"Open as / Source code",就可以在Xcode里,发现它就是一个XML格式的文本文件了:
接下来,我们就在App的Document目录下,创建一个EpisodeList.plist文件,用来保存获取的视频列表。
保存EpisodeListItem到plist文件
为了把视频列表保存在plist文件里,首先我们要对EpisodeListItem进行一些修改,让它从NSObject派生,并且遵从NSCoding protocol:
class EpisodeListItem : NSObject, NSCoding {
var title = ""
var finished = false
}
然后,我们来实现NSCoding中约定的方法,首先,我们来告诉iOS在保存EpisodeListItem对象的时候,如何"编码"它的属性:
class EpisodeListItem : NSObject, NSCoding {
// ...
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(title, forKey: "Title")
aCoder.encodeBool(finished, forKey: "Finished")
}
}
其中NSCoder用来把各种类型的值"编码"成适合保存在文件里的形式。于是,上面的方法意思是说,当保存一个EpisodeListItem对象的时候,把它的title属性用键值"Title"保存成一个对象,把它的finished属性用键值"Finished"保存成一个Bool值。
然后,我们要添加两个init方法:
class EpisodeListItem : NSObject, NSCoding {
// ...
required init?(coder aDecoder: NSCoder) {
super.init()
}
override init() {
super.init()
}
}
虽然它们没什么实际用途,为了让编译器通过编译通过,我们必须这样定义它们。
这样,iOS就知道如何把我们的EpisodeListItem对象序列化到一个plist文件了,接下来,我们回到EpisodeListViewController,把获取的视频列表写到EpisodeList.plist里:
class EpisodeListViewController: UITableViewController {
// ...
func saveEpisodeListItems() {
// 1\. Create a concrete archiver
let data = NSMutableData()
let archiver = NSKeyedArchiver(forWritingWithMutableData: data)
// 2\. Serialize object into archiver
archiver.encodeObject(episodeListItems, forKey: "EpisodeListItems")
archiver.finishEncoding()
// 3\. Create the file
data.writeToURL(fileUrl("EpisodeList.plist"), atomically: true)
}
}
从上面的代码可以看到,把一个对象写入到plist大体分成三步:
首先,我们创建一个NSMutableData对象,并且用它创建一个NSKeyedArchiver,这个archiver用来把我们要保存的内容编码到NSMutableData对象里;
其次,我们把要保存的数组,用一个键值"EpisodeListItems"通过archiver进行编码,在这里,由于我们之前让EpisodeListItem遵从了NSCoding protocol,因此archiver会按照我们的要求,对数组里的内容进行编码;编码完成后,我们用finishedEncoding()通知archiver;
此时,data里就包含编码过的我们要保存的内容了,我们使用writeToURL方法,直接把data写到我们指定的plist文件里;
完成之后,按Command + R编译执行:
在控制台里,我们用同样的方法复制Documents目录到Finder,我们就可以在里面看到EpisodeList.plist文件了,用Xcode打开它,可以看到类似这样的内容:
也许,这并不想你想象的那样易读,实际上,NSKeyedArchiver编码过的内容,它并不是用来给我们读的。接下来,我们就来看,如何从文件加载保存过的内容。
加载plist中的内容
就像我们要告诉iOS如何编码EpisodeListItem对象一样,为了读取它,我们也要告诉iOS如何解码它。打开EpisodeListItem.swift文件,添加下面的代码:
class EpisodeListItem : NSObject, NSCoding {
// ...
required init?(coder aDecoder: NSCoder) {
title = aDecoder.decodeObjectForKey("Title") as! String
finished = aDecoder.decodeBoolForKey("Finished")
super.init(coder: aCoder)
}
}
当我们通过加载EpisodeList.plist创建EpisodeLiteItem对象的时候,执行的代码和保存文件的时候是相反的。我们通过decodeObjectForKey,把plist文件中和Key对应的值读取出来。对于title来说,由于它被编码成了一个Object,因此解码出来的对象类型是AnyObject,我们要把它转换成一个String类型。
定义了解码EpisodeListItem的方式之后,回到EpisodeListViewController。我们应该把通过EpisodeList.plist初始化EpisodeListItems的代码放在哪呢?你可能会很自然的想到viewDidLoad啊。当然,这不会造成什么问题,但是,作为一个和任何UI显示都不相关的代码,我们应该把它放到EpisodeListViewController的init方法里。
当我们通过storyboard加载一个view controller时,iOS会调用一个和EpisodeListItem中类似的init方法,而这是我们初始化EpisodeListItems的绝佳机会:
class EpisodeListViewController: UITableViewController {
// ...
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
loadEpisodeListItems()
}
}
然后,我们来实现这个loadEpisodeListItems方法:
class EpisodeListViewController: UITableViewController {
// ...
func loadEpisodeListItems() {
let episodeUrl = fileUrl("EpisodeList.plist");
var error: NSError?
let fileExist =
episodeUrl.checkResourceIsReachableAndReturnError(&error)
if fileExist {
if let data = NSData(contentsOfURL: episodeUrl) {
let unarchiver =
NSKeyedUnarchiver(forReadingWithData: data)
episodeListItems =
unarchiver.decodeObjectForKey(
"EpisodeListItems") as! [EpisodeListItem]
unarchiver.finishDecoding()
}
}
else {
getEpisodeListItemData()
saveEpisodeListItems()
}
}
}
首先,我们使用了checkResourceIsReachableAndReturnError方法,判断文件是否存在。如果存在,我们用EpisodeList.plist文件的内容生成一个NSData对象,然后,我们基于这个NSData对象生成一个NSKeyedUnarchiver对象,并且,针对其中键值为EpisodeListItems的对象进行解码。
由于我们之前在EpisodeLitemItem类中,定义了解码方法,因此,我们就可以自动获得一个解码后的EpisodeListItem数组了。
否则,如果EpisodeList.plist文件不存在,我们就手动初始化EpisodeListItems数组,然后,新建一个EpisodeList.plist文件。
最后,我们删掉原来在viewDidLoad()中的初始化代码,Command + R编译执行,这次,我们就可以从EpisodeList.plist中,加载UITableView要显示的内容了。