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 UIApplicationDidEnterBackgroundNotification
notification.
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:
downloadImage
is executed via notifications and so the method receives the notification object as a parameter. TheUIImageView
and image URL are retrieved from the notification.- Retrieve the image from the
PersistencyManager
if it’s been downloaded previously. - If the image hasn’t already been downloaded, then retrieve it using
HTTPClient
. - 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
:
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:
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
Post a Comment