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

觀察者模式 - Observer

在觀察者模式里,一個(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)了觀察者模式: NotificationKey-Value Observing (KVO)。

通知 - Notification

不要把這里的通知和推送通知或者本地通知搞混了,這里的通知是基于訂閱-發(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.swiftinit 方法的 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)銷毀的對(duì)象,導(dǎo)致程序崩潰。

LibaratyAPI.swift 里加上取消訂閱的代碼:

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

當(dāng)對(duì)象銷毀的時(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 本身。 UIImageViewURL 都可以從其中獲取到。
  • 如果以前下載過(guò),從 PersistencyManager 里獲取緩存。
  • 如果圖片沒(méi)有緩存,則通過(guò) HTTPClient 獲取。
  • 如果下載完成,展示圖片并用 PersistencyManager 存儲(chǔ)到本地。

再回顧一下,我們使用外觀模式隱藏了下載圖片的復(fù)雜程度。通知的發(fā)送者并不在乎圖片是如何從網(wǎng)上下載到本地的。

運(yùn)行一下項(xiàng)目,可以看到專輯封面已經(jīng)顯示出來(lái)了:

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

關(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

在 KVO 里,對(duì)象可以注冊(cè)監(jiān)聽(tīng)任何屬性的變化,不管它是否持有。如果感興趣的話,可以讀一讀蘋(píng)果 KVO 編程指南。

如何使用 KVO

正如前面所提及的, 對(duì)象可以關(guān)注任何屬性的變化。在我們的例子里,我們可以用 KVO 關(guān)注 UIImageViewimage 屬性變化。

打開(kāi) AlbumView.swift 文件,找到 init(frame:albumCover:) 方法,在把 coverImage 添加到 subView 的代碼后面添加如下代碼:

coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)

這行代碼把 self (也就是當(dāng)前類) 添加到了 coverImageimage 屬性的觀察者里。

在銷毀的時(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)一切正常了:

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

注意:一定要記得移除觀察者,否則如果對(duì)象已經(jīng)銷毀了還給它發(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)的專輯并不會(huì)再下次打開(kāi)應(yīng)用的時(shí)候出現(xiàn)。

為了解決這個(gè)問(wèn)題,我們可以使用下一種模式:備忘錄模式。