(已完结)马上着手开发 iOS 应用程序 (十一) - 持久化数据

重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式对技术的选择及界面的设计开发进行规划。这些信息有可能发生变化,因此根据本文档的软件开发应当基于最终版本的操作系统和文档进行测试。该文档的新版本或许会随着API或相关技术未来的发展而进行更新。

翻译自苹果官网:

https://developer.apple.com/library/ios/referencelibrary/GettingStarted/DevelopiOSAppsSwift/Lesson10.html#//apple_ref/doc/uid/TP40015214-CH14-SW1

本课集中精力为 app 保存食物列表数据。数据持久化是 iOS app 开发中最重要和普遍的一个问题。iOS 有许多数据持久化方式;在本课中,使用 NSCoding 这种轻量级方式来归档对象和结构体。归档后的对象可以存储在硬盘并在之后被恢复。

学习目标

在课程的最后,你将能够:

  • 创建一个结构体
  • 理解静态属性和对象属性的区别
  • 使用 NSCoding 协议读写数据

存取食物

这一步将在 Meal 类中实现存取食物的功能。使用推荐的 NSCoding 方式,Meal 类负责存取其中的每个属性。需要给每个属性关联一个特殊的 key 值来保存数据,然后通过获取 key 关联的信息来加载数据。

key 是简单的字符串值。基于在 app 中的意义选择你自己的 keys。例如,你或许使用关键字 "name" 来存储 name 属性的值。

为了让关键字更加清晰地对应每条数据,创建一个结构体来存储 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 协议。为了遵循这个协议, Meal 需要是 NSObject 的子类。而 NSObject 为运行时系统定义了基本接口,是所有关于 Foundation 的基类。

成为 NSObject 子类并遵循 NSCoding
  1. 在 Meal.swift 中,找到 class 行:

     class Meal {
    
  2. 在 Meal 后面,添加冒号(:)和 NSObject 来成为 NSObject 类的的子类:

     class Meal: NSObject {
    
  3. 在 NSObject 后面,添加逗号(,)和 NSCoding 来遵循 NSCoding 协议:

     class Meal: NSObject, NSCoding {
    

NSCoding 协议定义了两个所有遵循它的类都必须实现的方法这样类的实例就可以被编码和解码了:

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

encodeWithCoder(_:) 方法准备类的信息来存档,当类创建时构造方法解档数据。需要为数据实现 encodeWithCoder(_:) 方法和构造方法来保存和加载。

实现 encodeWithCoder 这个 NSCoding 方法
  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)
}

编码方法写完,是时候实现构造方法来解码已编码的数据。

实现构造方法加载食物
  1. 在 encodeWithCoder(_:) 方法的下面,添加如下构造方法:

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

    required 关键字意味着这个类的每个子类如果定义了构造方法的同时必须实现这个构造方法。

    convenience 关键字表示便利构造器。便利构造器必须代理调用同一类中的其它指定构造器。指定构造器是类中最主要的构造器。一个指定构造器将初始化类中提供的所有属性,并根据父类链往上调用父类的构造器来实现父类的初始化。这里,你声明构造器为便利构造器因为只在加载已经保存数据的时候运用它。
    问号(?)意味着这是可失败构造器有可能返回 nil。

  2. 添加下面这行代码:

     let name = aDecoder.decodeObjectForKey(PropertyKey.nameKey) as! String
    

decodeObjectForKey(_:) 方法解档关于对象存储的信息。

decodeObjectForKey(_:) 的返回值是 AnyObject,下转上面的代码作为一个字符串并赋值给 name 常量。

  1. 在前一行下面,添加如下代码:

     // 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。你需要考虑到这两种情况。

  2. 在前一行的下面,添加这行代码:

     let rating = aDecoder.decodeIntegerForKey(PropertyKey.ratingKey)
    

    decodeIntegerForKey(_:) 方法解档一个整形。因为 decodeIntegerForKey 返回值是 Int,所以不需要转换值。

  3. 在最后添加如下代码:

     // 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:) 是指定构造器,它的实现需要调用父类的构造器。

更新构造器实现来调用它父类的构造器。
  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! 这种语法来访问路径。

检验:使用 Command-B 编译 app。编译应该没有问题。

保存和加载食物列表

现在可以存取单份食物了,当用户添加,编辑或者删除一份食物时候你需要存取食物列表。

实现方法保存食物列表
  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 类定义的常量 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! 这个路径的对象并下转这个对象为食物对象数组。这行代码使用 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()
    

    当用户添加一份新的食物或更新已存在的就会保存食物数组。确保这行代码写在 if 语句的外面。

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

    当删除一份食物时保存食物数组。

你的 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
     }
    

    如果 loadMeals() 成功返回 Meal 对象的数组,条件为 true 然后 if 语句得到执行。如果 loadMeals() 返回 nil,不会读取食物且 if 语句不会执行。

  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 时仍然在那里。

注意:

为了看到本课完整示例项目,下载文件并在 Xcode 中查看它。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 在本课中,你将在FoodTracker应用会话中保存菜品列表。理解并实现数据持久化是iOS应用开发的重要组成部分。...
    raingu24阅读 914评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,471评论 16 22
  • 今天感恩节哎,感谢一直在我身边的亲朋好友。感恩相遇!感恩不离不弃。 中午开了第一次的党会,身份的转变要...
    迷月闪星情阅读 10,548评论 0 11
  • 可爱进取,孤独成精。努力飞翔,天堂翱翔。战争美好,孤独进取。胆大飞翔,成就辉煌。努力进取,遥望,和谐家园。可爱游走...
    赵原野阅读 2,713评论 1 1
  • 在妖界我有个名头叫胡百晓,无论是何事,只要找到胡百晓即可有解决的办法。因为是只狐狸大家以讹传讹叫我“倾城百晓”,...
    猫九0110阅读 3,254评论 7 3