The Observer Pattern

Cocoa implements the observer pattern in two familiar ways: Notifications and Key-Value Observing (KVO).

Notifications

Not be be confused with Push or Local notifications, Notifications are based on a subscribe-and-publish model that allows an object (the publisher) to send messages to other objects (subscribers/listeners). The publisher never needs to know anything about the subscribers.
Notifications are heavily used by Apple. For example, when the keyboard is shown/hidden the system sends a UIKeyboardWillShowNotification/UIKeyboardWillHideNotification, respectively. When your app goes to the background, the system sends a UIApplicationDidEnterBackgroundNotificationnotification.
Note: Open up UIApplication.swift, at the end of the file you’ll see a list of over 20 notifications sent by the system.

How to Use Notifications

Go to AlbumView.swift and insert the following code to the end of the init(frame: CGRect, albumCover: String) initializer:
NSNotificationCenter.defaultCenter().postNotificationName("BLDownloadImageNotification", object: self, userInfo: ["imageView":coverImage, "coverUrl" : albumCover])
This line sends a notification through the NSNotificationCenter singleton. The notification info contains the UIImageView to populate and the URL of the cover image to be downloaded. That’s all the information you need to perform the cover download task.
Add the following line to init in LibraryAPI.swift, directly after super.init():
NSNotificationCenter.defaultCenter().addObserver(self, selector:"downloadImage:", name: "BLDownloadImageNotification", object: nil)
This is the other side of the equation: the observer. Every time an AlbumView class posts a BLDownloadImageNotification notification, since LibraryAPI has registered as an observer for the same notification, the system notifies LibraryAPI. Then LibraryAPI calls downloadImage() in response.
However, before you implement downloadImage() you must remember to unsubscribe from this notification when your class is deallocated. If you do not properly unsubscribe from a notification your class registered for, a notification might be sent to a deallocated instance. This can result in application crashes.
Add the following method to LibraryAPI.swift:
deinit {
  NSNotificationCenter.defaultCenter().removeObserver(self)
}
When this object is deallocated, it removes itself as an observer from all notifications it had registered for.
There’s one more thing to do. It would probably be a good idea to save the downloaded covers locally so the app won’t need to download the same covers over and over again.
Open PersistencyManager.swift and add the methods below:
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!)
  }
}
This code is pretty straightforward. The downloaded images will be saved in the Documents directory, and getImage() will return nil if a matching file is not found in the Documents directory.
Now add the following method to 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! String
 
    //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 as String)
          //4
          dispatch_sync(dispatch_get_main_queue(), { () -> Void in
            imageViewUnWrapped.image = downloadedImage
            self.persistencyManager.saveImage(downloadedImage, filename: coverUrl.lastPathComponent)
          })
        })
      }
    }
  }
Here’s a breakdown of the above code:
  1. downloadImage is executed via notifications and so the method receives the notification object as a parameter. The UIImageView and image URL are retrieved from the notification.
  2. Retrieve the image from the PersistencyManager if it’s been downloaded previously.
  3. If the image hasn’t already been downloaded, then retrieve it using HTTPClient.
  4. When the download is complete, display the image in the image view and use the PersistencyManager to save it locally.
Again, you’re using the Facade pattern to hide the complexity of downloading an image from the other classes. The notification sender doesn’t care if the image came from the web or from the file system.
Build and run your app and check out the beautiful covers inside your HorizontalScroller:
swiftDesignPattern13
Stop your app and run it again. Notice that there’s no delay in loading the covers because they’ve been saved locally. You can even disconnect from the Internet and your app will work flawlessly. However, there’s one odd bit here: the spinner never stops spinning! What’s going on?
You started the spinner when downloading the image, but you haven’t implemented the logic to stop the spinner once the image is downloaded. You could send out a notification every time an image has been downloaded, but instead, you’ll do that using the other Observer pattern, KVO.

Key-Value Observing (KVO)

In KVO, an object can ask to be notified of any changes to a specific property; either its own or that of another object. If you’re interested, you can read more about this on Apple’s KVO Programming Guide.

How to Use the KVO Pattern

As mentioned above, the KVO mechanism allows an object to observe changes to a property. In your case, you can use KVO to observe changes to the image property of the UIImageView that holds the image.
Open AlbumView.swift and add the following code to init(frame:albumCover:), just after you add coverImage as a subView:
coverImage.addObserver(self, forKeyPath: "image", options: nil, context: nil)
This adds self, which is the current class, as an observer for the image property of coverImage.
You also need to unregister as an observer when you’re done. Still in AlbumView.swift, add the following code:
deinit {
  coverImage.removeObserver(self, forKeyPath: "image")
}
Finally, add this method:
override func observeValueForKeyPath(keyPath: String, ofObject object: AnyObject, change: [NSObject : AnyObject], context: UnsafeMutablePointer<Void>) {
  if keyPath == "image" {
    indicator.stopAnimating()
  }
}
You must implement this method in every class acting as an observer. The system executes this method every time an observed property changes. In the above code, you stop the spinner when the “image” property changes. This way, when an image is loaded, the spinner will stop spinning.
Build and run your project. The spinner should disappear:
swiftDesignPattern14
Note: Always remember to remove your observers when they’re deallocated, or else your app will crash when the subject tries to send messages to these non-existent observers!

Comments

Popular posts from this blog

iOS Architecture

Property vs Instance Variable (iVar) in Objective-c [Small Concept but Great Understanding..]

setNeedsLayout vs layoutIfNeeded Explained