在觀察者模式里,一個(gè)對(duì)象在狀態(tài)變化的時(shí)候會(huì)通知另一個(gè)對(duì)象。參與者并不需要知道其他對(duì)象的具體是干什么的 - 這是一種降低耦合度的設(shè)計(jì)。這個(gè)設(shè)計(jì)模式常用于在某個(gè)屬性改變的時(shí)候通知關(guān)注該屬性的對(duì)象。
常見(jiàn)的使用方法是觀察者注冊(cè)監(jiān)聽(tīng),然后再狀態(tài)改變的時(shí)候,所有觀察者們都會(huì)收到通知。
在 MVC 里,觀察者模式意味著需要允許 Model
對(duì)象和 View
對(duì)象進(jìn)行交流,而不能有直接的關(guān)聯(lián)。
Cocoa
使用兩種方式實(shí)現(xiàn)了觀察者模式: Notification
和 Key-Value Observing (KVO)
。
不要把這里的通知和推送通知或者本地通知搞混了,這里的通知是基于訂閱-發(fā)布模型的,即一個(gè)對(duì)象 (發(fā)布者) 向其他對(duì)象 (訂閱者) 發(fā)送消息。發(fā)布者永遠(yuǎn)不需要知道訂閱者的任何數(shù)據(jù)。
Apple
對(duì)于通知的使用很頻繁,比如當(dāng)鍵盤(pán)彈出或者收起的時(shí)候,系統(tǒng)會(huì)分別發(fā)送 UIKeyboardWillShowNotification/UIKeyboardWillHideNotification
的通知。當(dāng)你的應(yīng)用切到后臺(tái)的時(shí)候,又會(huì)發(fā)送 UIApplicationDidEnterBackgroundNotification
的通知。
注意:打開(kāi) UIApplication.swift
文件,在文件結(jié)尾你會(huì)看到二十多種系統(tǒng)發(fā)送的通知。
打開(kāi) AlbumView.swift
然后在 init
的最后插入如下代碼:
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
這行代碼通過(guò) NSNotificationCenter
發(fā)送了一個(gè)通知,通知信息包含了 UIImageView
和圖片的下載地址。這是下載圖像需要的所有數(shù)據(jù)。
然后在 LibraryAPI.swift
的 init
方法的 super.init()
后面加上如下代碼:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil)
這是等號(hào)的另一邊:觀察者。每當(dāng) AlbumView
發(fā)出一個(gè) BLDownloadImageNotification
通知的時(shí)候,由于 LibraryAPI
已經(jīng)注冊(cè)了成為觀察者,所以系統(tǒng)會(huì)調(diào)用 downloadImage()
方法。
但是,在實(shí)現(xiàn) downloadImage()
之前,我們必須先在 dealloc
里取消監(jiān)聽(tīng)。如果沒(méi)有取消監(jiān)聽(tīng)消息,消息會(huì)發(fā)送給一個(gè)已經(jīng)銷(xiāo)毀的對(duì)象,導(dǎo)致程序崩潰。
在 LibaratyAPI.swift
里加上取消訂閱的代碼:
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self)
}
當(dāng)對(duì)象銷(xiāo)毀的時(shí)候,把它從所有消息的訂閱列表里去除。
這里還要做一件事情:我們最好把圖片存儲(chǔ)到本地,這樣可以避免一次又一次下載相同的封面。
打開(kāi) 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!)
}
}
代碼很簡(jiǎn)單直接,下載的圖片會(huì)存儲(chǔ)在 Documents
目錄下,如果沒(méi)有檢查到緩存文件, getImage()
方法則會(huì)返回 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
通過(guò)通知調(diào)用,所以這個(gè)方法的參數(shù)就是 NSNotification
本身。 UIImageView
和 URL
都可以從其中獲取到。PersistencyManager
里獲取緩存。HTTPClient
獲取。PersistencyManager
存儲(chǔ)到本地。再回顧一下,我們使用外觀模式隱藏了下載圖片的復(fù)雜程度。通知的發(fā)送者并不在乎圖片是如何從網(wǎng)上下載到本地的。
運(yùn)行一下項(xiàng)目,可以看到專(zhuān)輯封面已經(jīng)顯示出來(lái)了:
關(guān)了應(yīng)用再重新運(yùn)行,注意這次沒(méi)有任何延時(shí)就顯示了所有的圖片,因?yàn)槲覀円呀?jīng)有了本地緩存。我們甚至可以在沒(méi)有網(wǎng)絡(luò)的情況下正常使用我們的應(yīng)用。不過(guò)出了問(wèn)題:這個(gè)用來(lái)提示加載網(wǎng)絡(luò)請(qǐng)求的小菊花怎么一直在顯示!
我們?cè)谙螺d圖片的時(shí)候開(kāi)啟了這個(gè)白色小菊花,但是在圖片下載完畢的時(shí)候我們并沒(méi)有停掉它。我們可以在每次下載成功的時(shí)候發(fā)送一個(gè)通知,但是我們不這樣做,這次我們來(lái)用用另一個(gè)觀察者模式: KVO 。
在 KVO 里,對(duì)象可以注冊(cè)監(jiān)聽(tīng)任何屬性的變化,不管它是否持有。如果感興趣的話,可以讀一讀蘋(píng)果 KVO 編程指南。
正如前面所提及的, 對(duì)象可以關(guān)注任何屬性的變化。在我們的例子里,我們可以用 KVO 關(guān)注 UIImageView
的 image
屬性變化。
打開(kāi) AlbumView.swift
文件,找到 init(frame:albumCover:)
方法,在把 coverImage
添加到 subView
的代碼后面添加如下代碼:
coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
這行代碼把 self
(也就是當(dāng)前類(lèi)) 添加到了 coverImage
的 image
屬性的觀察者里。
在銷(xiāo)毀的時(shí)候,我們也需要取消觀察。還是在 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()
}
}
必須在所有的觀察者里實(shí)現(xiàn)上面的代碼。在檢測(cè)到屬性變化的時(shí)候,系統(tǒng)會(huì)自動(dòng)調(diào)用這個(gè)方法。在上面的代碼里,我們?cè)趫D片加載完成的時(shí)候把那個(gè)提示加載的小菊花去掉了。
再次運(yùn)行項(xiàng)目,你會(huì)發(fā)現(xiàn)一切正常了:
注意:一定要記得移除觀察者,否則如果對(duì)象已經(jīng)銷(xiāo)毀了還給它發(fā)送消息會(huì)導(dǎo)致應(yīng)用崩潰。
此時(shí)你可以把玩一下當(dāng)前的應(yīng)用然后再關(guān)掉它,你會(huì)發(fā)現(xiàn)你的應(yīng)用的狀態(tài)并沒(méi)有存儲(chǔ)下來(lái)。最后看見(jiàn)的專(zhuān)輯并不會(huì)再下次打開(kāi)應(yīng)用的時(shí)候出現(xiàn)。
為了解決這個(gè)問(wèn)題,我們可以使用下一種模式:備忘錄模式。
更多建議: