備忘錄模式
備忘錄模式 - Memento
備忘錄模式捕捉並且具象化一個物件的內在狀態。換句話說,它把你的物件存在了某個地方,然後在以後的某個時間再把它恢復出來,而不會打破它本身的封裝性,私有資料依舊是私有資料。
如何使用備忘錄模式
在ViewController.swift
里加上下面兩個方法:
//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
把當前相簿的索引值存到NSUserDefaults
裡。NSUserDefaults
是 iOS 提供的一個標準儲存方案,用於儲存應用的配置資訊和資料。
loadPreviousState
方法載入上次儲存的索引值。這並不是備忘錄模式的完整實現,但是已經離目標不遠了。
接下來在viewDidLoad
的scroller.delegate = self
前面呼叫:
loadPreviousState()
這樣在剛初始化的時候就載入了上次儲存的狀態。但是什麼時候儲存當前狀態呢?這個時候我們可以用通知來做。在應用進入到後臺的時候, iOS 會發送一個UIApplicationDidEnterBackgroundNotification
的通知,我們可以在這個通知裡呼叫saveCurrentState
這個方法。是不是很方便?
在viewDidLoad
的最後加上如下程式碼:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
現在,當應用即將進入後臺的時候,ViewController
會呼叫saveCurrentState
方法自動儲存當前狀態。
當然也別忘了取消監聽通知,新增如下程式碼:
deinit { NSNotificationCenter.defaultCenter().removeObserver(self) }
這樣就確保在ViewController
銷燬的時候取消監聽通知。
這時再執行程式,隨意移到某個專輯上,然後按下 Home 鍵把應用切換到後臺,再在 Xcode 上把 App 關閉。重新啟動,會看見上次記錄的專輯已經存了下來併成功還原了:

image
看起來專輯資料好像是對了,但是上面的滾動條似乎出了問題,沒有居中啊!
這時initialViewIndex
方法就派上用場了。由於在委託裡 (也就是ViewController
) 還沒實現這個方法,所以初始化的結果總是第一張專輯。
為了修復這個問題,我們可以在ViewController.swift
裡新增如下程式碼:
func initialViewIndex(scroller: HorizontalScroller) -> Int { return currentAlbumIndex }
現在HorizontalScroller
可以根據currentAlbumIndex
自動滑到相應的索引位置了。
再次重複上次的步驟,切到後臺,關閉應用,重啟,一切順利:

image
回頭看看PersistencyManager
的init
方法,你會發現專輯資料是我們硬編碼寫進去的,而且每次建立PersistencyManager
的時候都會再建立一次專輯資料。而實際上一個比較好的方案是隻建立一次,然後把專輯資料存到本地檔案裡。我們如何把專輯資料存到檔案裡呢?
一種方案是遍歷Album
的屬性然後把它們寫到一個plist
檔案裡,然後如果需要的時候再重新建立Album
物件。這並不是最好的選擇,因為資料和屬性不同,你的程式碼也就要相應的產生變化。舉個例子,如果我們以後想新增Movie
物件,它有著完全不同的屬性,那麼儲存和讀取資料又需要重寫新的程式碼。
況且你也無法儲存這些物件的私有屬性,因為其他類是沒有訪問許可權的。這也就是為什麼 Apple 提供了 歸檔 的機制。
歸檔 - Archiving
蘋果通過歸檔的方法來實現備忘錄模式。它把物件轉化成了流然後在不暴露內部屬性的情況下儲存資料。你可以讀一讀 《iOS 6 by Tutorials》 這本書的第 16 章,或者看下[蘋果的歸檔和序列化文件][14]。
如何使用歸檔
首先,我們需要讓Album
實現NSCoding
協議,宣告這個類是可被歸檔的。開啟Album.swift
在class
那行後面加上NSCoding
:
class Album: NSObject, NSCoding {
然後新增如下的兩個方法:
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
的一部分,在被歸檔的時候呼叫。相對的,init(coder:)
方法則是用來解檔的。很簡單,很強大。
現在Album
物件可以被歸檔了,新增一些程式碼來儲存和載入Album
資料。
在PersistencyManager.swift
裡新增如下程式碼:
func saveAlbums() { var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin") let data = NSKeyedArchiver.archivedDataWithRootObject(albums) data.writeToFile(filename, atomically: true) }
這個方法可以用來儲存專輯。NSKeyedArchiver
把專輯陣列歸檔到了albums.bin
這個檔案裡。
當我們歸檔一個包含子物件的物件時,系統會自動遞迴的歸檔子物件,然後是子物件的子物件,這樣一層層遞迴下去。在我們的例子裡,我們歸檔的是albums
因為Array
和Album
都是實現NSCopying
介面的,所以數組裡的物件都可以自動歸檔。
用下面的程式碼取代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() }
我們把建立專輯資料的方法放到了createPlaceholderAlbum
裡,這樣程式碼可讀性更高。在新的程式碼裡,如果存在歸檔檔案,NSKeyedUnarchiver
從歸檔檔案載入資料;否則就建立歸檔檔案,這樣下次程式啟動的時候可以讀取本地檔案載入資料。
我們還想在每次程式進入後臺的時候儲存專輯資料。看起來現在這個功能並不是必須的,但是如果以後我們加了編輯功能,這樣做還是很有必要的,那時我們肯定希望確保新的資料會同步到本地的歸檔檔案。
因為我們的程式通過LibraryAPI
來訪問所有服務,所以我們需要通過LibraryAPI
來通知PersistencyManager
儲存專輯資料。
在LibraryAPI
裡新增儲存專輯資料的方法:
func saveAlbums() { persistencyManager.saveAlbums() }
這個方法很簡單,就是把LibraryAPI
的saveAlbums
方法傳遞給了persistencyManager
的saveAlbums
方法。
然後在ViewController.swift
的saveCurrentState
方法的最後加上:
LibraryAPI.sharedInstance.saveAlbums()
在ViewController
需要儲存狀態的時候,上面的程式碼通過LibraryAPI
歸檔當前的專輯資料。
執行一下程式,檢查一下沒有編譯錯誤。
不幸的是似乎沒什麼簡單的方法來檢查歸檔是否正確完成。你可以檢查一下Documents
目錄,看下是否存在歸檔檔案。如果要檢視其他資料變化的話,還需要新增編輯專輯資料的功能。
不過和編輯資料相比,似乎加個刪除專輯的功能更好一點,如果不想要這張專輯直接刪除即可。再進一步,萬一誤刪了話,是不是還可以再加個撤銷按鈕?