• 持久化数据


    本章的重点是跨越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时。

     

     

  • 相关阅读:
    【codeforces 604D】Moodular Arithmetic
    【hdu 1846】Brave Game
    【hdu 2147】kiki's game
    【hdu 2188】选拔志愿者
    【hdu 2149】Public Sale
    【hdu 1847】Good Luck in CET-4 Everybody!
    【hdu 1849】Rabbit and Grass
    FreeBSD 内核中的SYSINIT分析【转】
    解决tomcat运行报错java.lang.UnsatisfiedLinkError: apache-tomcat-7.0.37in cnative-1.dll:Can load AMD 64
    菜单的事件推送
  • 原文地址:https://www.cnblogs.com/tianjian/p/4633466.html
Copyright © 2020-2023  润新知