您的位置:首页 > 其它

持久化数据

2015-07-09 16:07 267 查看
本章的重点是跨越FoodTracker app会话来保存meal list数据。数据持久性是iOS开发最重要最常见的问题之一。iOS有很多持久化数据存储的解决方案。在本章中,你可以使用NSCoding作为数据持久化机制.NSCoding是一个协议,它允许轻量级的解决方案来存档对象和其他结构。存档对象能存储到磁盘中并能检索。这个类似android中的SharedPreferences。

[b]学习目标[/b]

在课程结束,你能学到

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.接着添加如下代码

[code]// 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来访问路径

[b]保存和载入Meal List[/b]

现在你可以保存和载入每一个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时。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: