Design Patterns in iOS Using Swift – Part 2/2

In the second part of this two-part tutorial on design patterns in Swift, you’ll learn more about adapter, observer, and memento patterns and how to apply them to your own apps. By Lorenzo Boaro.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

The Observer Pattern

In the Observer pattern, one object notifies other objects of any state changes. The objects involved don't need to know about one another - thus encouraging a decoupled design. This pattern's most often used to notify interested objects when a property has changed.

The usual implementation requires that an observer registers interest in the state of another object. When the state changes, all the observing objects are notified of the change.

If you want to stick to the MVC concept (hint: you do), you need to allow Model objects to communicate with View objects, but without direct references between them. And that's where the Observer pattern comes in.

Cocoa implements the observer pattern in two 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 UIKeyboardWillShow/UIKeyboardWillHide, respectively. When your app goes to the background, the system sends a UIApplicationDidEnterBackground notification.

How to Use Notifications

Right click on RWBlueLibrary and select New Group. Rename it Extension. Right click again on that group and select New File.... Select iOS > Swift File and set the file name to NotificationExtension.swift.

Copy the following code inside the file:

extension Notification.Name {
  static let BLDownloadImage = Notification.Name("BLDownloadImageNotification")
}

You are extending Notification.Name with your custom notification. From now on, the new notification can be accessed as .BLDownloadImage, just as you would a system notification.

Go to AlbumView.swift and insert the following code to the end of the init(frame:coverUrl:) method:

NotificationCenter.default.post(name: .BLDownloadImage, object: self, userInfo: ["imageView": coverImageView, "coverUrl" : coverUrl])

This line sends a notification through the NotificationCenter 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, as the implementation of the currently empty init:

NotificationCenter.default.addObserver(self, selector: #selector(downloadImage(with:)), name: .BLDownloadImage, object: nil)

This is the other side of the equation: the observer. Every time an AlbumView posts a BLDownloadImage notification, since LibraryAPI has registered as an observer for the same notification, the system notifies LibraryAPI. Then LibraryAPI calls downloadImage(with:) in response.

Before you implement downloadImage(with:) 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. After the import Foundation, add the following line:

import UIKit

This import is important because you will deal with UI objects, like UIImage.

Add this computed property to the end of the class:

private var cache: URL {
  return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
}

This variable returns the URL of the cache directory, which is a good place to store files that you can re-download at any time.

Now add these two methods:

func saveImage(_ image: UIImage, filename: String) {
  let url = cache.appendingPathComponent(filename)
  guard let data = UIImagePNGRepresentation(image) else {
    return
  }
  try? data.write(to: url)
}

func getImage(with filename: String) -> UIImage? {
  let url = cache.appendingPathComponent(filename)
  guard let data = try? Data(contentsOf: url) else {
    return nil
  }
  return UIImage(data: data)
}

This code is pretty straightforward. The downloaded images will be saved in the Cache directory, and getImage(with:) will return nil if a matching file is not found in the Cache directory.

Now open LibraryAPI.swift and add import UIKit after the first available import.

At the end of the class add the following method:

@objc func downloadImage(with notification: Notification) {
  guard let userInfo = notification.userInfo,
    let imageView = userInfo["imageView"] as? UIImageView,
    let coverUrl = userInfo["coverUrl"] as? String,
    let filename = URL(string: coverUrl)?.lastPathComponent else {
      return
  }
  
  if let savedImage = persistencyManager.getImage(with: filename) {
    imageView.image = savedImage
    return
  }
  
  DispatchQueue.global().async {
    let downloadedImage = self.httpClient.downloadImage(coverUrl) ?? UIImage()
    DispatchQueue.main.async {
      imageView.image = downloadedImage
      self.persistencyManager.saveImage(downloadedImage, filename: filename)
    }
  }
}

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 collection view:

Album app showing cover art but still with spinners

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 property just below the private var indicatorView: UIActivityIndicatorView! declaration:

private var valueObservation: NSKeyValueObservation!

Now add the following code to commonInit, just before you add the cover image view as a subview:

valueObservation = coverImageView.observe(\.image, options: [.new]) { [unowned self] observed, change in
  if change.newValue is UIImage {
      self.indicatorView.stopAnimating()
  }
}

This snippet of code adds the image view as an observer for the image property of the cover image. \.image is the key path expression that enables this mechanism.

In Swift 4, a key path expression has the following form:

\<type>.<property>.<subproperty>

The type can often be inferred by the compiler, but at least 1 property needs to be provided. In some cases, it might make sense to use properties of properties. In your case, the property name, image has been specified, while the type name UIImageView has been omitted.

The trailing closure specifies the closure that is executed 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:

How the album app will look when the design patterns tutorial is complete

Note: Always remember to remove your observers when they're deinited, or else your app will crash when the subject tries to send messages to these non-existent observers! In this case the valueObservation will be deinited when the album view is, so the observing will stop then.

If you play around with your app a bit and terminate it, you'll notice that the state of your app isn't saved. The last album you viewed won't be the default album when the app launches.

To correct this, you can make use of the next pattern on the list: Memento.