持久化数据

本章的重点是跨越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(_:)方法解档一个整型。因为ofdecodeIntegerForKey返回的就是一个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时。

 

 

posted @ 2015-07-09 16:07  jy02432443  阅读(501)  评论(0编辑  收藏  举报