重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。
翻译自苹果官网:
本课集中精力为 app 保存食物列表数据。数据持久化是 iOS app 开发中最重要和普遍的一个问题。iOS 有许多数据持久化方式;在本课中,使用 NSCoding 这种轻量级方式来归档对象和结构体。归档后的对象可以存储在硬盘并在之后被恢复。
学习目标
在课程的最后,你将能够:
- 创建一个结构体
- 理解静态属性和对象属性的区别
- 使用 NSCoding 协议读写数据
存取食物
这一步将在 Meal 类中实现存取食物的功能。使用推荐的 NSCoding 方式,Meal 类负责存取其中的每个属性。需要给每个属性关联一个特殊的 key 值来保存数据,然后通过获取 key 关联的信息来加载数据。
key 是简单的字符串值。基于在 app 中的意义选择你自己的 keys。例如,你或许使用关键字 "name" 来存储 name
属性的值。
为了让关键字更加清晰地对应每条数据,创建一个结构体来存储 key 字符串。使用这种方式,当你需要在代码各处使用 keys 时,使用常量而不是重新输入字符串(减少错误的可能性)。
实现一个 coding key 结构体
打开 Meal.swift。
-
在 Meal.swift 的 // MARK: Properties 区域下面,添加这个结构体:
// MARK: Types struct PropertyKey { }
-
在 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 协议。为了遵循这个协议, Meal 需要是 NSObject 的子类。而 NSObject 为运行时系统定义了基本接口,是所有关于 Foundation 的基类。
成为 NSObject 子类并遵循 NSCoding
-
在 Meal.swift 中,找到 class 行:
class Meal {
-
在 Meal 后面,添加冒号(:)和 NSObject 来成为 NSObject 类的的子类:
class Meal: NSObject {
-
在 NSObject 后面,添加逗号(,)和 NSCoding 来遵循 NSCoding 协议:
class Meal: NSObject, NSCoding {
NSCoding 协议定义了两个所有遵循它的类都必须实现的方法这样类的实例就可以被编码和解码了:
func encodeWithCoder(aCoder: NSCoder)
init(coder aDecoder: NSCoder)
encodeWithCoder(_:) 方法准备类的信息来存档,当类创建时构造方法解档数据。需要为数据实现 encodeWithCoder(_:)
方法和构造方法来保存和加载。
实现 encodeWithCoder 这个 NSCoding 方法
-
在 Meal.swift 最后的大括号(})前面,添加下面注释:
// MARK: NSCoding
这行注释帮助你(和其他读你代码的人)知道在这个区域的代码关联着数据持久化。
-
在注释下面,添加这个方法:
func encodeWithCoder(aCoder: NSCoder) { }
-
在 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)
}
编码方法写完,是时候实现构造方法来解码已编码的数据。
实现构造方法加载食物
-
在 encodeWithCoder(_:) 方法的下面,添加如下构造方法:
required convenience init?(coder aDecoder: NSCoder) { }
required 关键字意味着这个类的每个子类如果定义了构造方法的同时必须实现这个构造方法。
convenience 关键字表示便利构造器。便利构造器必须代理调用同一类中的其它指定构造器。指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。这里,你声明构造器为便利构造器因为只在加载已经保存数据的时候运用它。
问号(?)意味着这是可失败构造器有可能返回 nil。 -
添加下面这行代码:
let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
decodeObjectForKey(_:) 方法解档关于对象存储的信息。
decodeObjectForKey(_:) 的返回值是 AnyObject,下转上面的代码作为一个字符串并赋值给 name 常量。
-
在前一行下面,添加如下代码:
// Because photo is an optional property of Meal, use conditional cast. let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage
将 decodeObjectForKey(_:) 的返回值下转为 UIImage 对象并赋值给一个 photo 常量。在这种情况下,使用可选类型转换符(as?)下转,因为图片属性是可选的,所以值可能是 UIImage,也可能是 nil。你需要考虑到这两种情况。
-
在前一行的下面,添加这行代码:
let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey)
decodeIntegerForKey(_:) 方法解档一个整形。因为 decodeIntegerForKey 返回值是 Int,所以不需要转换值。
-
在最后添加如下代码:
// 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)
}
因为你在 Meal 类中定义的 init?(name:photo:rating:)
是指定构造器,它的实现需要调用父类的构造器。
更新构造器实现来调用它父类的构造器。
-
找到如下构造器:
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 } }
-
在 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! 这种语法来访问路径。
检验:使用 Command-B 编译 app。编译应该没有问题。
保存和加载食物列表
现在可以存取单份食物了,当用户添加,编辑或者删除一份食物时候你需要存取食物列表。
实现方法保存食物列表
打开 MealTableViewController.swift。
-
在 MealTableViewController.swift 的最后大括号(})前面,添加如下注释:
// MARK: NSCoding
这行注释帮助你和其他读你代码的人知道此区域代码有关数据持久化。
-
在注释下面,添加这个方法:
func saveMeals() { }
-
在 saveMeals() 方法中,添加下行代码:
let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path!)
方法尝试解档 meals 数组到一个特定的位置,如果成功返回 true。使用 Meal 类定义的常量 Meal.ArchiveURL 来判断保存信息的位置。
但是如何快速测试数据是否保存成功?使用 print 打印信息到控制台。例如,如果食物保存失败打印一个失败消息。
-
在前一行的下面,添加如下 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...")
}
}
现在,实现一个方法来加载保存过的食物。
实现方法加载食物列表
-
在 MealTableViewController.swift 最后大括号(})的前面,添加如下方法:
func loadMeals() -> [Meal]? { }
方法返回 Meal 对象可选数组集合,意味着可能返回一个食物对象数组或者什么都不返回(nil)。
-
在 loadMeals() 方法里面,添加这行代码:
return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as? [Meal]
方法尝试解档 Meal.ArchiveURL.path! 这个路径的对象并下转这个对象为食物对象数组。这行代码使用 as? 操作符这样允许返回 nil。因为数组有可能没有存储过,就会下转失败,在这种情况下应该返回 nil。
你的 loadMeals() 方法应该像这样:
func loadMeals() -> [Meal]? {
return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as? [Meal]
}
当用户添加,删除或编辑食物后保存食物列表
-
在
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) } } }
-
在 else 从句右边,添加如下代码:
// Save the meals. saveMeals()
当用户添加一份新的食物或更新已存在的就会保存食物数组。确保这行代码写在 if 语句的外面。
-
在
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 } }
-
在
meals.removeAtIndex(indexPath.row)
行后面,添加下行代码:saveMeals()
当删除一份食物时保存食物数组。
你的 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
}
}
现在食物在合适的时候就会保存了,同样需要确保食物在合适的时候读取出来。应该在每次食物列表场景加载的时候做这个工作,这意味着最合适读取数据的地方就是在 viewDidLoad 中。
在合适时候读取食物列表
-
在 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() }
-
在第二行代码
navigationItem.leftBarButtonItem = editButtonItem()
后面,添加如下 if 语句:// Load any saved meals, otherwise load sample data. if let savedMeals = loadMeals() { meals += savedMeals }
如果
loadMeals()
成功返回 Meal 对象的数组,条件为 true 然后 if 语句得到执行。如果 loadMeals() 返回 nil,不会读取食物且 if 语句不会执行。 -
在 if 语句后面,添加 else 从句并移动 loadSampleMeals() 的调用到它里面:
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 时仍然在那里。
注意:
为了看到本课完整示例项目,下载文件并在 Xcode 中查看它。