备忘录模式 – Memento
最后更新于:2022-04-01 00:46:04
备忘录模式捕捉并且具象化一个对象的内在状态。换句话说,它把你的对象存在了某个地方,然后在以后的某个时间再把它恢复出来,而不会打破它本身的封装性,私有数据依旧是私有数据。
### 如何使用备忘录模式
在 `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 关闭。重新启动,会看见上次记录的专辑已经存了下来并成功还原了:
[![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern15-202x320.png)](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern15-202x320.png)
看起来专辑数据好像是对了,但是上面的滚动条似乎出了问题,没有居中啊!
这时 `initialViewIndex` 方法就派上用场了。由于在委托里 (也就是 `ViewController` ) 还没实现这个方法,所以初始化的结果总是第一张专辑。
为了修复这个问题,我们可以在 `ViewController.swift` 里添加如下代码:
~~~
func initialViewIndex(scroller: HorizontalScroller) -> Int {
return currentAlbumIndex
}
~~~
现在 `HorizontalScroller` 可以根据 `currentAlbumIndex` 自动滑到相应的索引位置了。
再次重复上次的步骤,切到后台,关闭应用,重启,一切顺利:
[![](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern16-230x320.png)](http://cdn2.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern16-230x320.png)
回头看看 `PersistencyManager` 的 `init` 方法,你会发现专辑数据是我们硬编码写进去的,而且每次创建 `PersistencyManager` 的时候都会再创建一次专辑数据。而实际上一个比较好的方案是只创建一次,然后把专辑数据存到本地文件里。我们如何把专辑数据存到文件里呢?
一种方案是遍历 `Album` 的属性然后把它们写到一个 `plist` 文件里,然后如果需要的时候再重新创建 `Album` 对象。这并不是最好的选择,因为数据和属性不同,你的代码也就要相应的产生变化。举个例子,如果我们以后想添加 `Movie` 对象,它有着完全不同的属性,那么存储和读取数据又需要重写新的代码。
况且你也无法存储这些对象的私有属性,因为其他类是没有访问权限的。这也就是为什么 Apple 提供了 归档 的机制。
### 归档 - Archiving
苹果通过归档的方法来实现备忘录模式。它把对象转化成了流然后在不暴露内部属性的情况下存储数据。你可以读一读 《iOS 6 by Tutorials》 这本书的第 16 章,或者看下[苹果的归档和序列化文档](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Archiving/Archiving.html)。
#### 如何使用归档
首先,我们需要让 `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` 目录,看下是否存在归档文件。如果要查看其他数据变化的话,还需要添加编辑专辑数据的功能。
不过和编辑数据相比,似乎加个删除专辑的功能更好一点,如果不想要这张专辑直接删除即可。再进一步,万一误删了话,是不是还可以再加个撤销按钮?