iOS Apps 开发(Swift)(11)——Persist Data

来源:互联网 发布:天津流氓 知乎 编辑:程序博客网 时间:2024/05/22 19:02

前言:网上一直没有找到用Swift开发IOS的好的教程,所以找了官网的文档翻译一下算了。如有错误欢迎指正。博主首发CSDN,mcf171专栏。

博客链接:mcf171的博客

原文链接:Implement Edit and Delete Behavior

——————————————————————————————

在本次课程中,我们将关注如何通过我们的FoodTracker app 来保存菜品列表。数据持久化是iOS app开发中最重要和常见的问题之一。iOS有很多持久化数据存储的解决方案。在本次课程中,我们将使用 NSCoding作为数据吃就好的机制。 NSCoding 是一个轻量级的接口。

学习目标

在本次课程中,你可以了解到:

  • 创建一个结构体
  • 理解静态属性和实例属性的区别
  • 施工 NSCoding接口来读写数据

保存和加载菜品

在这一部中我们将实现Meal 类的保存和读取的功能。使用了 NSCoding方法之后,Meal类能够对于他自己的每一个属性进行存储和加载。数据的存储和加载都是通过制定的key来进行操作。比如说我们可以使用key “name” 来存储name 的属性值

为了能共对已每一个数据来编码对应的key,我们需要创建一个结构体来存储这些key字符串。通过这样的方式,我们可以在我们工程的任一地方都是用结构体常量,而不是直接写这些字符串。

实现 key 的结构体

1、打开 Meal.swift

2、在Meal.swift中的 //MARK: Properties 小节下添加结构体

// MARK: Types struct PropertyKey {}

3、在结构体中添加属性

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是为运行时系统定义了一个基本的接口类。

成为NSObject 的子类,并遵循NSCoding
1、在Meal.swift中找到类的声明

class Meal {

2、在Meal后面加一个冒号:然后添加NSObject

class Meal: NSObject {

3、在NSObject后加一个逗号,然后添加NSCoding来采用NSCoding接口

class Meal: NSObject, NSCoding {

NSCoding接口有两个方法,对于采用它的类来说必须实现这两个方法。

func encodeWithCoder(aCoder: NSCoder)init(coder aDecoder: NSCoder)

encodeWithCoder方法是为类的信息归档做预准备的。当类被创建的时候初始化器回解归档数据。因此我们需要实现保存和加载功能,我们需要实现encodewithCode方法和初始化器方法。

实现encodeWithCoder 方法

1、在Meal.swift文件最后一个花括号前添加以下注释

// MARK: NSCoding

2、在注释下添加以下方法

func encodeWithCoder(aCoder: NSCoder) {}

3、在上述方法中添加以下代码

aCoder.encodeObject(name, forKey: PropertyKey.nameKey)aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)

encodeObject方法可以编码任何类型的对象,上述代码的目的就是将Meal类中每一个属性通过他们对应存储的key值来进行编码。

最终encodewithCode为:

func encodeWithCoder(aCoder: NSCoder) {    aCoder.encodeObject(name, forKey: PropertyKey.nameKey)    aCoder.encodeObject(photo, forKey: PropertyKey.photoKey)    aCoder.encodeInteger(rating, forKey: PropertyKey.ratingKey)}
实现初始化器来加载菜品
1、在encodewithCode方法下添加初始化器方法

required convenience init?(coder aDecoder: NSCoder) {}

required的关键字意味着对于基类来说,它的子类都必须定义和实现初始化器。

(以下这段楼主也没太懂,建议看英文原文,就不强行翻译了)

convenience关键字则表明将这个初始化器作为一个便利的初始化器。便利的初始化器最终会被类指定的初始化器之一来调用。指定的初始化器是类的主要初始化器(省略若干)

2、添加以下代码

let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
decodeObjectForKey方法解归档了存储在一个对象中的信息。返回的是一个AnyObject的类型值,所以我们需要将它向下转型为String。我们使用的是强类型转换操作符!,因为如果对象不能强转为String,那么就会为nil值。也就会报运行时错误。

3、在上面的代码下添加以下代码

// Because photo is an optional property of Meal, use conditional cast.let photo = aDecoder.decodeObjectForKey(PropertyKey.photoKey) as? UIImage

因为photo 属性是一个optional对象,所以我们选择了optional类型的转换运算符。

4、继续添加以下代码

let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey)

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)}

因为我们在Meal类中定义的其他初始化器 init?(name:photo:ration:)是一个特定的初始化器,因此它的实现需要调用父类的初始化器

更新初始化器的实现来调用父类的初始化器

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: 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.ArchiveURL.path!来访问路径

里程碑:使用Command + B编译我们的app,没有任何错误

保存和加载菜品列表

现在我们可以保存和加载菜品了。

实现保存菜品列表的方法

1、打开MealTableViewController.swift

2、在最后一个花括号前添加以下注释

// MARK: NSCoding

3、在上述注视下添加以下方法

func saveMeals() {}

4、在上述方法添加以下代码

let isSuccessfulSave = NSKeyedArchiver.archiveRootObject(meals, toFile: Meal.ArchiveURL.path!)
上述方法尝试将meals数据归档到指定的位置,如果成功的话返回true。使用常量Meal.ArchiveURL来获取存储信息的位置。

那么我们如何测试数据是否保存成功呢?我们可以使用print来向控制台打印信息。

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...")    }}

现在我们要实现加载保存的菜品列表

实现加载菜品列表的方法

1、在MealTableViewController.swift文件的最后一个花括号增加以下代码

func loadMeals() -> [Meal]? {}
这个方法会返回一个可选的 Meal 对象数组,也就是可能会返回一个数组,或者一个空的nil

2、在 loadMeals()方法里面,添加以下代码

return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as? [Meal]
这个方法尝试从Meal.archiveURL.path!位置的数据进行解归档,并且向下转型为Meal 对象的数组。之所以使用 as?操作符是因为可能返回的是nil。因为存储的数组可能有也可能没有。因为我们的向下转型可能失败,所以我们的方法可能返回nil。

loadMeals方法应该为:

func loadMeals() -> [Meal]? {    return NSKeyedUnarchiver.unarchiveObjectWithFile(Meal.ArchiveURL.path!) as? [Meal]}

随着这些方法的实现,我们需要增加代码来实现当用户增删改查的时候保存和加载菜品列表。

当用户增删改查的时候保存和加载菜品列表。

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()
这个代码将会在用户添加一个新的菜品或者更新一个菜品的时候调用保存菜品数组。

3、找到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()
上述代码将在当一个菜品删除之后会保存菜品数组
我们的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方法中加载数据。

在合适的时候加载菜品列表
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())后增加下面的 if 语句

// Load any saved meals, otherwise load sample data.if let savedMeals = loadMeals() {    meals += savedMeals}
当 if 语句执行并且条件为true的话,loadMeals成功调用会返回一个Meal对象数组,如果loadMeals会返回nil,也就是没有菜品加载。上述代码会增加一些菜品。

3、在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的时候显示

ps:完整的项目工程链接Download File



0 0