本章的重点是跨越FoodTracker app会话来保存meal list数据。数据持久性是iOS开发最重要最常见的问题之一。iOS有很多持久化数据存储的解决方案。在本章中,你可以使用NSCoding作为数据持久化机制.NSCoding是一个协议,它允许轻量级的解决方案来存档对象和其他结构。存档对象能存储到磁盘中并能检索。这个类似android中的SharedPreferences。
学习目标
在课程结束,你能学到
1.创建一个结构体
2.理解静态数据和实例属性的区别
3.使用NSCoding协议读取和写入数据
保存和载入Meal
在这个步骤中我们将会在Meal类中实现保存和载入meal的行为。使用NSCoding方法,Meal类负责存储和载入每一个属性。它需要通过分配给每一个值到一个特别的key中来保存它的数据,并通过关联的key来查询信息并载入数据。
一个key是一个简单的字符串值。你选择自己的key根据使用什么样的场景。例如,你可以使用key:“name”作为存储name属性值。
为了弄清楚哪一个key对应的每一块数据,可以创建结构体来存储key的字符串。这样一来,当你在多个地方需要使用keys时,你能使用常量来代替硬编码
实现coding key结构体
1.打开Meal.swift
2.在Meal.swift的注释(// MARK: Properties)下方添加如下代码
// MARK: Types struct PropertyKey { }
3.在PropertyKey结构体中,添加这些情况:
static let nameKey = "name" static let photoKey = "photo" static let ratingKey = "rating"
每一个常量对应Meal中的每一个属性。static关键字表示这个常量应用于结构体自生,而不是一个结构体实例。这些值将永远不会改变。
你的PropertyKey结构体看起来如下
struct PropertyKey { static let nameKey = "name" static let photoKey = "photo" static let ratingKey = "rating" }
为了能编码和解码它自己和它的属性,Meal类需要确认是否符合NSCoding协议。为了符合NSCoding协议,Meal还必须为NSObject的子类。NSObject被认为是一个顶层基类
继承NSObject并符合NSCoding协议
1.在Meal.swift中,找到class这行
class Meal {
2.在Meal后添加冒号并添加NSObject,表示当前Meal为NSObject的子类
class Meal: NSObject {
3.在NSObject后面,添加逗号和NSCoding,表示来采用NSObject
协议
class Meal: NSObject, NSCoding {
NSCoding协议中,声明了两个方法,并且必须实现这两个方法,分别是编码和解码:
func encodeWithCoder(aCoder: NSCoder)
init(coder aDecoder: NSCoder)
encodeWithCoder(_:)
方法准备归档类的信息,当类创建时,init()方法,用来解档数据。你需要实现这两个方法,用来保存和载入属性
实现encodeWithCoder()方法
1.在Meal.swift的(?)上方,添加如下代码
// MARK: NSCoding
2.在注释下方,添加方法
func encodeWithCoder(aCoder: NSCoder) {
}
3.在encodeWithCoder(_:)方法内,添加如下代码
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)
aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)
encodeObject(_:forKey:)方法是用来编码任意对象类型,encodeInteger(_:forKey:)是用来编码整型。这几行代码把Meal类的每一个属性值,编码存储到它们对应的key中
完整的encodeWithCoder(_:)方法如下
func encodeWithCoder(aCoder: NSCoder) {
aCoder.encodeObject(name, forKey: PropertyKey.nameKey)
aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)
aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)
}
当我们写完编码方法后,接下来我们要写解码方法init了
实现init来载入meal
1.在encodeWithCoder(_:)方法下方,添加init方法
required convenience init?(coder aDecoder: NSCoder) {
}
required关键字表示每一个定义了init的子类必须实现这个init
convenience
关键字表示这个初始化方法作为一个便利初始化(convenience initializer),便利初始化作为次要的初始化,它必须通过类中特定的初始化来调用。特定初始化(Designated initializers)是首要初始化。它们完全的通过父类初始化来初始化所有引入的属性,并继续初始化父类。这里,你声明的初始化是便利初始化,因为它仅用于保存和载入数据时。问号表示它是一个failable的初始化,即可能返回nil
2.在方法中添加以下代码
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
decodeObjectForKey(_:)方法解档已存储的信息,返回的值是AnyObject,子类强转作为一个String来分配给name常量。你使用强制类型转换操作符(as!)来子类强转一个返回值。因为如果对象不能强转成String,或为nil,那么会发生错误并在运行时崩溃。
3.接着添加如下代码
// Because photo is an optional property of Meal, use conditional cast. let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage
你通过decodeObjectForKey(_:)子类强转为UIImage类型。由于photo属性是一个可选值,所以UIImage可能会nil。你需要考虑两种情况。
4.接着添加如下代码
let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey)
decodeIntegerForKey(_:)方法解档一个整型。因为of
decodeIntegerForKey返回的就是一个Int,所以不需要子类强转解码。
5.接着添加如下代码
// Must call designated initilizer. self.init(name: name, photo: photo, rating: rating)
作为一个便利初始化,这个初始化需要被特定初始化来调用它。你可以一些参数来保存数据。
完整的init?(coder:)方法如下所示
required convenience init?(coder aDecoder: NSCoder) { let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String // Because photo is an optional property of Meal, use conditional cast. let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey) // Must call designated initializer. self.init(name: name, photo: photo, rating: rating) }
我们先前已经创建过init?(name:photo:rating:)函数了,它是一个特定初始化,实现这个init,需要调用父类的初始化函数
更新特定初始化函数,让其调用父类的初始化
1.找到特定初始化函数,看起来如下
init?(name: String, photo: UIImage?, rating: Int) { // Initialize stored properties. self.name = name self.photo = photo self.rating = rating // Initialization should fail if there is no name or if the rating is negative. if name.isEmpty || rating < 0 { return nil } }
2.在self.rating = rating下方,添加一个父类初始化函数的调用
super.init()
完整的 init?(name:photo:rating:)函数如下
init?(name: String, photo: UIImage?, rating: Int) { // Initialize stored properties. self.name = name self.photo = photo self.rating = rating super.init() // Initialization should fail if there is no name or if the rating is negative. if name.isEmpty || rating < 0 { return nil } }
接下来,你需要一个持久化的文件系统路径,这是存放保存和载入数据的地方。你需要知道去哪里找它。你添加的路径声明在类的外部,标记为一个全局常量
创建一个文件路径
在Meal.swift中,// MARK: Properties
下方添加如下代码
// MARK: Archiving Paths static let DocumentsDirectory = NSFileManager().URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first! static let ArchiveURL = DocumentsDirectory.URLByAppendingPathComponent("meals")
你使用static关键字来声明这些常量,表示它们可用于Meal类的外部,你可以使用Meal.ArchiveURL.path来访问路径
保存和载入Meal List
现在你可以保存和载入每一个meal,每当用户添加,编辑,删除一个菜谱时,你需要保存和载入meal list
实现保存meal list的方法
1.打开 MealTableViewController.swift
2.在 MealTableViewController.swift中,在(})上方,添加如下代码
// MARK: NSCoding
3.在注释下方添加以下方法
func saveMeals() {
}
4.在saveMeals()方法中,添加以下代码
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path!)
这个方法试图归档meals数组到一个指定的路径中,如果成功,则返回true。它使用了常量Meal.ArchiveURL.path,来保存信息到这个路径中
但你如果快速的测试数据是否保存成功呢?你可以在控制台使用print来输出isSuccessfulSave变量值。
5.接下来添加if语句
if !isSuccessfulSave { print("Failed to save meals...") }
如果保存失败,你会在控制台看到这个输出消息
完整的saveMeals()方法看起来如下
func saveMeals() { let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path!) if !isSuccessfulSave { print("Failed to save meals...") } }
接下来我们需要实现载入的方法
实现载入meal list的方法
1.在MealTableViewController.swift中的(})上方,添加如下方法
func loadMeals() -> [Meal]? {
}
这个方法返回一个可选的Meal对象数组类型,它可能返回一个Meal数组对象或返回nil
2.在loadMeals()方法中,添加如下代码
return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchivePath!) as? [Meal]
这个方法试图解档存储在Meal.ArchiveURL.path路径下的对象,并子类强转为一个Meal对象数组。代码使用(as?)操作符,所以它可能返回nil。这表示子类强转可能会失败,在这种情况下方法会返回nil
完整的loadMeals()方法如下
func loadMeals() -> [Meal]? { return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as? [Meal] }
保存和载入方法已经实现了,接下来我们需要在几种场合下来调用它们。
当用户添加,移除,编辑菜谱时,调用保存meal list的方法
1.在MealTableViewController.swift中,找到unwindToMealList(_:)动作方法
@IBAction func unwindToMealList(sender: UIStoryboardSegue) { if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal { if let selectedIndexPath = tableView.indexPathForSelectedRow { // Update an existing meal. meals[selectedIndexPath.row] = meal tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None) } else { // Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0) meals.append(meal) tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom) } } }
2.在else语法体的下方,添加如下代码
// Save the meals. saveMeals()
上面的代码会保存meals数组,每当一个新的菜谱被添加,或一个已存在的菜谱被更新时。
3.在MealTableViewController.swift中,找到tableView(_:commitEditingStyle:forRowAtIndexPath:)方法
// Override to support editing the table view. override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { // Delete the row from the data source meals.removeAtIndex(indexPath.row) tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } else if editingStyle == .Insert { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } }
4.在meals.removeAtIndex(indexPath.row)下方,添加如下代码
saveMeals()
这行代码是在一个菜谱被删除后,保存meals数组
完整的unwindToMealList(_:)动作方法如下
@IBAction func unwindToMealList(sender: UIStoryboardSegue) { if let sourceViewController = sender.sourceViewController as? MealViewController, meal = sourceViewController.meal { if let selectedIndexPath = tableView.indexPathForSelectedRow { // Update an existing meal. meals[selectedIndexPath.row] = meal tableView.reloadRowsAtIndexPaths([selectedIndexPath], withRowAnimation: .None) } else { // Add a new meal. let newIndexPath = NSIndexPath(forRow: meals.count, inSection: 0) meals.append(meal) tableView.insertRowsAtIndexPaths([newIndexPath], withRowAnimation: .Bottom) } // Save the meals. saveMeals() } }
完整的tableView(_:commitEditingStyle:forRowAtIndexPath:)方法如下
// Override to support editing the table view. override func tableView(tableView: UITableView, commitEditingStyle editingStyle: UITableViewCellEditingStyle, forRowAtIndexPath indexPath: NSIndexPath) { if editingStyle == .Delete { // Delete the row from the data source meals.removeAtIndex(indexPath.row) saveMeals() tableView.deleteRowsAtIndexPaths([indexPath], withRowAnimation: .Fade) } else if editingStyle == .Insert { // Create a new instance of the appropriate class, insert it into the array, and add a new row to the table view } }
现在会在适当的时间保存,你需要确保meals在适当的时间被载入。它应该发生在每次meal list场景被载入时,这个合适的地方应该是在viewDidLoad()方法中来载入已经存储的数据
在适当的时候载入meal list
1.在 MealTableViewController.swift中,找到viewDidLoad()方法
override func viewDidLoad() { super.viewDidLoad() // Use the edit button item provided by the table view controller. navigationItem.leftBarButtonItem = editButtonItem() // Load the sample data. loadSampleMeals() }
2.在navigationItem.leftBarButtonItem = editButtonItem()下方添加如下代码
// Load any saved meals, otherwise load sample data. if let savedMeals = loadMeals() { meals += savedMeals }
如果loadMeals()方法成功地返回Meal对象数组,那么if表达式为true,并执行if语法体中的代码。否则如果返回nil,则表示没有meals载入。
3.在if语句后,添加else语句,用来载入样本Meals
else { // Load the sample data. loadSampleMeals() }
你完整的viewDidLoad()方法如下
override func viewDidLoad() { super.viewDidLoad() // Use the edit button item provided by the table view controller. navigationItem.leftBarButtonItem = editButtonItem() // Load any saved meals, otherwise load sample data. if let savedMeals = loadMeals() { meals += savedMeals } else { // Load the sample data. loadSampleMeals() } }
检查站:执行你的app。如果你添加了新的菜谱并退出app后,已添加的菜谱将出现在你下次打开app时。