在觀察者模式里,一個對象在狀態變化的時候會通知另一個對象。參與者并不需要知道其他對象的具體是干什么的 - 這是一種降低耦合度的設計。這個設計模式常用于在某個屬性改變的時候通知關注該屬性的對象。
常見的使用方法是觀察者注冊監聽,然后再狀態改變的時候,所有觀察者們都會收到通知。
在 MVC 里,觀察者模式意味著需要允許?`Model`?對象和?`View`?對象進行交流,而不能有直接的關聯。
`Cocoa`?使用兩種方式實現了觀察者模式:?`Notification`?和?`Key-Value Observing (KVO)`。
### 通知 - Notification
不要把這里的通知和推送通知或者本地通知搞混了,這里的通知是基于訂閱-發布模型的,即一個對象 (發布者) 向其他對象 (訂閱者) 發送消息。發布者永遠不需要知道訂閱者的任何數據。
`Apple`?對于通知的使用很頻繁,比如當鍵盤彈出或者收起的時候,系統會分別發送`UIKeyboardWillShowNotification/UIKeyboardWillHideNotification`?的通知。當你的應用切到后臺的時候,又會發送?`UIApplicationDidEnterBackgroundNotification`?的通知。
注意:打開?`UIApplication.swift`?文件,在文件結尾你會看到二十多種系統發送的通知。
#### 如何使用通知
打開?`AlbumView.swift`?然后在?`init`?的最后插入如下代碼:
~~~
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
~~~
這行代碼通過?`NSNotificationCenter`?發送了一個通知,通知信息包含了?`UIImageView`?和圖片的下載地址。這是下載圖像需要的所有數據。
然后在?`LibraryAPI.swift`?的?`init`?方法的?`super.init()`?后面加上如下代碼:
~~~
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil)
~~~
這是等號的另一邊:觀察者。每當?`AlbumView`?發出一個?`BLDownloadImageNotification`?通知的時候,由于?`LibraryAPI`?已經注冊了成為觀察者,所以系統會調用?`downloadImage()`?方法。
但是,在實現?`downloadImage()`?之前,我們必須先在?`dealloc`?里取消監聽。如果沒有取消監聽消息,消息會發送給一個已經銷毀的對象,導致程序崩潰。
在?`LibaratyAPI.swift`?里加上取消訂閱的代碼:
~~~
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
~~~
當對象銷毀的時候,把它從所有消息的訂閱列表里去除。
這里還要做一件事情:我們最好把圖片存儲到本地,這樣可以避免一次又一次下載相同的封面。
打開?`PersistencyManager.swift`?添加如下代碼:
~~~
func saveImage(image: UIImage, filename: String) {
let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)")
let data = UIImagePNGRepresentation(image)
data.writeToFile(path, atomically: true)
}
func getImage(filename: String) -> UIImage? {
var error: NSError?
let path = NSHomeDirectory().stringByAppendingString("/Documents/\(filename)")
let data = NSData(contentsOfFile: path, options: .UncachedRead, error: &error)
if let unwrappedError = error {
return nil
} else {
return UIImage(data: data!)
}
}
~~~
代碼很簡單直接,下載的圖片會存儲在?`Documents`?目錄下,如果沒有檢查到緩存文件,`getImage()`?方法則會返回?`nil`?。
然后在?`LibraryAPI.swift`?添加如下代碼:
~~~
func downloadImage(notification: NSNotification) {
//1
let userInfo = notification.userInfo as [String: AnyObject]
var imageView = userInfo["imageView"] as UIImageView?
let coverUrl = userInfo["coverUrl"] as NSString
//2
if let imageViewUnWrapped = imageView {
imageViewUnWrapped.image = persistencyManager.getImage(coverUrl.lastPathComponent)
if imageViewUnWrapped.image == nil {
//3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), { () -> Void in
let downloadedImage = self.httpClient.downloadImage(coverUrl)
//4
dispatch_sync(dispatch_get_main_queue(), { () -> Void in
imageViewUnWrapped.image = downloadedImage
self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent)
})
})
}
}
}
~~~
拆解一下上面的代碼:
* `downloadImage`?通過通知調用,所以這個方法的參數就是?`NSNotification`?本身。`UIImageView`?和?`URL`?都可以從其中獲取到。
* 如果以前下載過,從?`PersistencyManager`?里獲取緩存。
* 如果圖片沒有緩存,則通過?`HTTPClient`?獲取。
* 如果下載完成,展示圖片并用?`PersistencyManager`?存儲到本地。
再回顧一下,我們使用外觀模式隱藏了下載圖片的復雜程度。通知的發送者并不在乎圖片是如何從網上下載到本地的。
運行一下項目,可以看到專輯封面已經顯示出來了:
[](http://cdn1.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern13-288x320.png)
關了應用再重新運行,注意這次沒有任何延時就顯示了所有的圖片,因為我們已經有了本地緩存。我們甚至可以在沒有網絡的情況下正常使用我們的應用。不過出了問題:這個用來提示加載網絡請求的小菊花怎么一直在顯示!
我們在下載圖片的時候開啟了這個白色小菊花,但是在圖片下載完畢的時候我們并沒有停掉它。我們可以在每次下載成功的時候發送一個通知,但是我們不這樣做,這次我們來用用另一個觀察者模式: KVO 。
### 鍵值觀察 - KVO
在 KVO 里,對象可以注冊監聽任何屬性的變化,不管它是否持有。如果感興趣的話,可以讀一讀[蘋果 KVO 編程指南](https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/KeyValueObserving/KeyValueObserving.html)。
#### 如何使用 KVO
正如前面所提及的, 對象可以關注任何屬性的變化。在我們的例子里,我們可以用 KVO 關注`UIImageView`?的?`image`?屬性變化。
打開?`AlbumView.swift`?文件,找到?`init(frame:albumCover:)`?方法,在把?`coverImage`?添加到?`subView`?的代碼后面添加如下代碼:
~~~
coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
~~~
這行代碼把?`self`?(也就是當前類) 添加到了?`coverImage`?的?`image`?屬性的觀察者里。
在銷毀的時候,我們也需要取消觀察。還是在?`AlbumView.swift`?文件里,添加如下代碼:
~~~
deinit {
coverImage.removeObserver(self, forKeyPath: "image")
}
~~~
最終添加如下方法:
~~~
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
if keyPath == "image" {
indicator.stopAnimating()
}
}
~~~
必須在所有的觀察者里實現上面的代碼。在檢測到屬性變化的時候,系統會自動調用這個方法。在上面的代碼里,我們在圖片加載完成的時候把那個提示加載的小菊花去掉了。
再次運行項目,你會發現一切正常了:
[](http://cdn3.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern14-292x320.png)
注意:一定要記得移除觀察者,否則如果對象已經銷毀了還給它發送消息會導致應用崩潰。
此時你可以把玩一下當前的應用然后再關掉它,你會發現你的應用的狀態并沒有存儲下來。最后看見的專輯并不會再下次打開應用的時候出現。
為了解決這個問題,我們可以使用下一種模式:備忘錄模式。