鍍金池/ 教程/ iOS/ 備忘錄模式 - Memento
開始
裝飾者模式 - Decorator
單例模式 - Singleton
外觀模式 - Facade
觀察者模式 - Observer
準(zhǔn)備工作
iOS 設(shè)計(jì)模式
適配器模式 - Adapter
備忘錄模式 - Memento
最后的潤色
小結(jié)
設(shè)計(jì)模式之王- MVC

備忘錄模式 - Memento

備忘錄模式捕捉并且具象化一個(gè)對(duì)象的內(nèi)在狀態(tài)。換句話說,它把你的對(duì)象存在了某個(gè)地方,然后在以后的某個(gè)時(shí)間再把它恢復(fù)出來,而不會(huì)打破它本身的封裝性,私有數(shù)據(jù)依舊是私有數(shù)據(jù)。

如何使用備忘錄模式

ViewController.swift 里加上下面兩個(gè)方法:

//MARK: Memento Pattern
func saveCurrentState() {
    // When the user leaves the app and then comes back again, he wants it to be in the exact same state
    // he left it. In order to do this we need to save the currently displayed album.
    // Since it's only one piece of information we can use NSUserDefaults.
    NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex")
}

func loadPreviousState() {
    currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex")
    showDataForAlbum(currentAlbumIndex)
}

saveCurrentState 把當(dāng)前相冊(cè)的索引值存到 NSUserDefaults 里。NSUserDefaults 是 iOS 提供的一個(gè)標(biāo)準(zhǔn)存儲(chǔ)方案,用于保存應(yīng)用的配置信息和數(shù)據(jù)。

loadPreviousState 方法加載上次存儲(chǔ)的索引值。這并不是備忘錄模式的完整實(shí)現(xiàn),但是已經(jīng)離目標(biāo)不遠(yuǎn)了。

接下來在 viewDidLoadscroller.delegate = self 前面調(diào)用:

    loadPreviousState()

這樣在剛初始化的時(shí)候就加載了上次存儲(chǔ)的狀態(tài)。但是什么時(shí)候存儲(chǔ)當(dāng)前狀態(tài)呢?這個(gè)時(shí)候我們可以用通知來做。在應(yīng)用進(jìn)入到后臺(tái)的時(shí)候, iOS 會(huì)發(fā)送一個(gè) UIApplicationDidEnterBackgroundNotification 的通知,我們可以在這個(gè)通知里調(diào)用 saveCurrentState 這個(gè)方法。是不是很方便?

viewDidLoad 的最后加上如下代碼:

NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)

現(xiàn)在,當(dāng)應(yīng)用即將進(jìn)入后臺(tái)的時(shí)候,ViewController 會(huì)調(diào)用 saveCurrentState 方法自動(dòng)存儲(chǔ)當(dāng)前狀態(tài)。

當(dāng)然也別忘了取消監(jiān)聽通知,添加如下代碼:

deinit {
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

這樣就確保在 ViewController 銷毀的時(shí)候取消監(jiān)聽通知。

這時(shí)再運(yùn)行程序,隨意移到某個(gè)專輯上,然后按下 Home 鍵把應(yīng)用切換到后臺(tái),再在 Xcode 上把 App 關(guān)閉。重新啟動(dòng),會(huì)看見上次記錄的專輯已經(jīng)存了下來并成功還原了:

http://wiki.jikexueyuan.com/project/ios-design-patterns-in-swift/images/11.png" alt="" />

看起來專輯數(shù)據(jù)好像是對(duì)了,但是上面的滾動(dòng)條似乎出了問題,沒有居中??!

這時(shí) initialViewIndex 方法就派上用場了。由于在委托里 (也就是 ViewController ) 還沒實(shí)現(xiàn)這個(gè)方法,所以初始化的結(jié)果總是第一張專輯。

為了修復(fù)這個(gè)問題,我們可以在 ViewController.swift 里添加如下代碼:

func initialViewIndex(scroller: HorizontalScroller) -> Int {
    return currentAlbumIndex
}

現(xiàn)在 HorizontalScroller 可以根據(jù) currentAlbumIndex 自動(dòng)滑到相應(yīng)的索引位置了。

再次重復(fù)上次的步驟,切到后臺(tái),關(guān)閉應(yīng)用,重啟,一切順利:

http://wiki.jikexueyuan.com/project/ios-design-patterns-in-swift/images/12.png" alt="" />

回頭看看 PersistencyManagerinit 方法,你會(huì)發(fā)現(xiàn)專輯數(shù)據(jù)是我們硬編碼寫進(jìn)去的,而且每次創(chuàng)建 PersistencyManager 的時(shí)候都會(huì)再創(chuàng)建一次專輯數(shù)據(jù)。而實(shí)際上一個(gè)比較好的方案是只創(chuàng)建一次,然后把專輯數(shù)據(jù)存到本地文件里。我們?nèi)绾伟褜]嫈?shù)據(jù)存到文件里呢?

一種方案是遍歷 Album 的屬性然后把它們寫到一個(gè) plist 文件里,然后如果需要的時(shí)候再重新創(chuàng)建 Album 對(duì)象。這并不是最好的選擇,因?yàn)閿?shù)據(jù)和屬性不同,你的代碼也就要相應(yīng)的產(chǎn)生變化。舉個(gè)例子,如果我們以后想添加 Movie 對(duì)象,它有著完全不同的屬性,那么存儲(chǔ)和讀取數(shù)據(jù)又需要重寫新的代碼。

況且你也無法存儲(chǔ)這些對(duì)象的私有屬性,因?yàn)槠渌愂菦]有訪問權(quán)限的。這也就是為什么 Apple 提供了 歸檔 的機(jī)制。

歸檔 - Archiving

蘋果通過歸檔的方法來實(shí)現(xiàn)備忘錄模式。它把對(duì)象轉(zhuǎn)化成了流然后在不暴露內(nèi)部屬性的情況下存儲(chǔ)數(shù)據(jù)。你可以讀一讀 《iOS 6 by Tutorials》 這本書的第 16 章,或者看下[蘋果的歸檔和序列化文檔][14]。

如何使用歸檔

首先,我們需要讓 Album 實(shí)現(xiàn) NSCoding 協(xié)議,聲明這個(gè)類是可被歸檔的。打開 Album.swiftclass 那行后面加上 NSCoding

class Album: NSObject, NSCoding {

然后添加如下的兩個(gè)方法:

required init(coder decoder: NSCoder) {
    super.init()
    self.title = decoder.decodeObjectForKey("title") as String?
    self.artist = decoder.decodeObjectForKey("artist") as String?
    self.genre = decoder.decodeObjectForKey("genre") as String?
    self.coverUrl = decoder.decodeObjectForKey("cover_url") as String?
    self.year = decoder.decodeObjectForKey("year") as String?
}

func encodeWithCoder(aCoder: NSCoder) {
    aCoder.encodeObject(title, forKey: "title")
    aCoder.encodeObject(artist, forKey: "artist")
    aCoder.encodeObject(genre, forKey: "genre")
    aCoder.encodeObject(coverUrl, forKey: "cover_url")
    aCoder.encodeObject(year, forKey: "year")
}

encodeWithCoder 方法是 NSCoding 的一部分,在被歸檔的時(shí)候調(diào)用。相對(duì)的, init(coder:) 方法則是用來解檔的。很簡單,很強(qiáng)大。

現(xiàn)在 Album 對(duì)象可以被歸檔了,添加一些代碼來存儲(chǔ)和加載 Album 數(shù)據(jù)。

PersistencyManager.swift 里添加如下代碼:

func saveAlbums() {
    var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")
    let data = NSKeyedArchiver.archivedDataWithRootObject(albums)
    data.writeToFile(filename, atomically: true)
} 

這個(gè)方法可以用來存儲(chǔ)專輯。 NSKeyedArchiver 把專輯數(shù)組歸檔到了 albums.bin 這個(gè)文件里。

當(dāng)我們歸檔一個(gè)包含子對(duì)象的對(duì)象時(shí),系統(tǒng)會(huì)自動(dòng)遞歸的歸檔子對(duì)象,然后是子對(duì)象的子對(duì)象,這樣一層層遞歸下去。在我們的例子里,我們歸檔的是 albums 因?yàn)?ArrayAlbum 都是實(shí)現(xiàn) NSCopying 接口的,所以數(shù)組里的對(duì)象都可以自動(dòng)歸檔。

用下面的代碼取代 PersistencyManager 中的 init 方法:

override init() {
    super.init()
    if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) {
        let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as [Album]?
        if let unwrappedAlbum = unarchiveAlbums {
            albums = unwrappedAlbum
        }
    } else {
        createPlaceholderAlbum()
    }
}

func createPlaceholderAlbum() {
    //Dummy list of albums
    let album1 = Album(title: "Best of Bowie",
             artist: "David Bowie",
             genre: "Pop",
             coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png",
             year: "1992")

    let album2 = Album(title: "It's My Life",
           artist: "No Doubt",
           genre: "Pop",
           coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png",
           year: "2003")

    let album3 = Album(title: "Nothing Like The Sun",
               artist: "Sting",
           genre: "Pop",
           coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png",
           year: "1999")

    let album4 = Album(title: "Staring at the Sun",
           artist: "U2",
           genre: "Pop",
           coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png",
           year: "2000")

    let album5 = Album(title: "American Pie",
           artist: "Madonna",
           genre: "Pop",
           coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png",
           year: "2000")
    albums = [album1, album2, album3, album4, album5]
    saveAlbums()
}

我們把創(chuàng)建專輯數(shù)據(jù)的方法放到了 createPlaceholderAlbum 里,這樣代碼可讀性更高。在新的代碼里,如果存在歸檔文件, NSKeyedUnarchiver 從歸檔文件加載數(shù)據(jù);否則就創(chuàng)建歸檔文件,這樣下次程序啟動(dòng)的時(shí)候可以讀取本地文件加載數(shù)據(jù)。

我們還想在每次程序進(jìn)入后臺(tái)的時(shí)候存儲(chǔ)專輯數(shù)據(jù)??雌饋憩F(xiàn)在這個(gè)功能并不是必須的,但是如果以后我們加了編輯功能,這樣做還是很有必要的,那時(shí)我們肯定希望確保新的數(shù)據(jù)會(huì)同步到本地的歸檔文件。

因?yàn)槲覀兊某绦蛲ㄟ^ LibraryAPI 來訪問所有服務(wù),所以我們需要通過 LibraryAPI 來通知 PersistencyManager 存儲(chǔ)專輯數(shù)據(jù)。

LibraryAPI 里添加存儲(chǔ)專輯數(shù)據(jù)的方法:

func saveAlbums() {
    persistencyManager.saveAlbums()
}

這個(gè)方法很簡單,就是把 LibraryAPIsaveAlbums 方法傳遞給了 persistencyManagersaveAlbums 方法。

然后在 ViewController.swiftsaveCurrentState 方法的最后加上:

LibraryAPI.sharedInstance.saveAlbums()

ViewController 需要存儲(chǔ)狀態(tài)的時(shí)候,上面的代碼通過 LibraryAPI 歸檔當(dāng)前的專輯數(shù)據(jù)。

運(yùn)行一下程序,檢查一下沒有編譯錯(cuò)誤。

不幸的是似乎沒什么簡單的方法來檢查歸檔是否正確完成。你可以檢查一下 Documents 目錄,看下是否存在歸檔文件。如果要查看其他數(shù)據(jù)變化的話,還需要添加編輯專輯數(shù)據(jù)的功能。

不過和編輯數(shù)據(jù)相比,似乎加個(gè)刪除專輯的功能更好一點(diǎn),如果不想要這張專輯直接刪除即可。再進(jìn)一步,萬一誤刪了話,是不是還可以再加個(gè)撤銷按鈕?

上一篇:開始下一篇:最后的潤色