持久化數據

本章的重點是跨越FoodTracker app會話來保存meal list數據。數據持久性是iOS開發最重要最多見的問題之一。iOS有不少持久化數據存儲的解決方案。在本章中,你可使用NSCoding做爲數據持久化機制.NSCoding是一個協議,它容許輕量級的解決方案來存檔對象和其餘結構。存檔對象能存儲到磁盤中並能檢索。這個相似android中的SharedPreferences。android

學習目標swift

在課程結束,你能學到數組

1.建立一個結構體app

2.理解靜態數據和實例屬性的區別ide

3.使用NSCoding協議讀取和寫入數據函數

保存和載入Meal學習

在這個步驟中咱們將會在Meal類中實現保存和載入meal的行爲。使用NSCoding方法,Meal類負責存儲和載入每個屬性。它須要經過分配給每個值到一個特別的key中來保存它的數據,並經過關聯的key來查詢信息並載入數據。測試

一個key是一個簡單的字符串值。你選擇本身的key根據使用什麼樣的場景。例如,你可使用key:「name」做爲存儲name屬性值。ui

爲了弄清楚哪個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時。