观察者模式 – Observer

最后更新于:2022-04-01 00:46:02

在观察者模式里,一个对象在状态变化的时候会通知另一个对象。参与者并不需要知道其他对象的具体是干什么的 - 这是一种降低耦合度的设计。这个设计模式常用于在某个属性改变的时候通知关注该属性的对象。 常见的使用方法是观察者注册监听,然后再状态改变的时候,所有观察者们都会收到通知。 在 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)](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)](http://cdn3.raywenderlich.com/wp-content/uploads/2014/11/swiftDesignPattern14-292x320.png) 注意:一定要记得移除观察者,否则如果对象已经销毁了还给它发送消息会导致应用崩溃。 此时你可以把玩一下当前的应用然后再关掉它,你会发现你的应用的状态并没有存储下来。最后看见的专辑并不会再下次打开应用的时候出现。 为了解决这个问题,我们可以使用下一种模式:备忘录模式。
';