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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Design Patterns in iOS Using Swift – Part 2/2
35 mins
- Getting Started
- The Adapter Pattern
- How to Use the Adapter Pattern
- The Observer Pattern
- Notifications
- How to Use Notifications
- Key-Value Observing (KVO)
- How to Use the KVO Pattern
- The Memento Pattern
- How to Use the Memento Pattern
- Archiving and Serialization
- How to Use Archiving and Serialization
- Where to go from here?
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:
-
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 collection view:
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:
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.