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 Memento Pattern
The memento pattern captures and externalizes an object's internal state. In other words, it saves your stuff somewhere. Later on, this externalized state can be restored without violating encapsulation; that is, private data remains private.
How to Use the Memento Pattern
iOS uses the Memento pattern as part of State Restoration. You can find out more about it by reading our tutorial, but essentially it stores and re-applies your application's state so the user is back where they left things.
To activate state restoration in the app, open Main.storyboard. Select the Navigation Controller and, in the Identity Inspector, find the Restoration ID field and type NavigationController.
Select the Pop Music scene and enter ViewController for the same field. These IDs tell iOS that you're interested in restoring state for those view controllers when the app restarts.
Add the following code to AppDelegate.swift:
func application(_ application: UIApplication, shouldSaveApplicationState coder: NSCoder) -> Bool {
return true
}
func application(_ application: UIApplication, shouldRestoreApplicationState coder: NSCoder) -> Bool {
return true
}
This code turns on state restoration for your app as a whole. Now, add the following code to the Constants
enum in ViewController.swift:
static let IndexRestorationKey = "currentAlbumIndex"
This key will be used to save and restore the current album index. Add the following code:
override func encodeRestorableState(with coder: NSCoder) {
coder.encode(currentAlbumIndex, forKey: Constants.IndexRestorationKey)
super.encodeRestorableState(with: coder)
}
override func decodeRestorableState(with coder: NSCoder) {
super.decodeRestorableState(with: coder)
currentAlbumIndex = coder.decodeInteger(forKey: Constants.IndexRestorationKey)
showDataForAlbum(at: currentAlbumIndex)
horizontalScrollerView.reload()
}
Here you are saving the index (this will happen when your app enters the background) and restoring it (this will happen when the app is launched, after the view of your view controller is loaded). After you restore the index, you update the table and scroller to reflect the updated selection. There's one more thing to be done - you need to move the scroller to the right position. It won't look right if you move the scroller here, because the views haven't yet been laid out. Add the following code to move the scroller at the right point:
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
horizontalScrollerView.scrollToView(at: currentAlbumIndex, animated: false)
}
Build and run your app. Navigate to one of the albums, send the app to the background with the Home button (Command+Shift+H if you are on the simulator) and then shut down your app from Xcode. Relaunch, and check that the previously selected album is the one centered:
If you look at PersistencyManager
's init
, you'll notice the album data is hardcoded and recreated every time PersistencyManager
is created. But it's better to create the list of albums once and store them in a file. How would you save the Album
data to a file?
One option is to iterate through Album
's properties, save them to a plist file and then recreate the Album
instances when they're needed. This isn't the best option, as it requires you to write specific code depending on what data/properties are there in each class. For example, if you later created a Movie
class with different properties, the saving and loading of that data would require new code.
Additionally, you won't be able to save the private variables for each class instance since they are not accessible to an external class. That's exactly why Apple created archiving and serialization mechanisms.
Archiving and Serialization
One of Apple's specialized implementations of the Memento pattern can be achieved through archiving and serialization. Before Swift 4, to serialize and archive your custom types you'd have to jump through a number of steps. For class types you'd need to subclass NSObject
and conform to NSCoding
protocol.
Value types like struct
and enum
required a sub object that can extend NSObject
and conform to NSCoding
.
Swift 4 resolves this issue for all these three types: class
, struct
and enum
[SE-0166].
How to Use Archiving and Serialization
Open Album.swift and declare that Album
implements Codable
. This protocol is the only thing required to make a Swift type Encodable
and Decodable
. If all properties are Codable
, the protocol implementation is automatically generated by the compiler.
Now your code should look like this:
struct Album: Codable {
let title : String
let artist : String
let genre : String
let coverUrl : String
let year : String
}
To actually encode the object, you'll need to use an encoder. Open PersistencyManager.swift and add the following code:
private var documents: URL {
return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
}
private enum Filenames {
static let Albums = "albums.json"
}
func saveAlbums() {
let url = documents.appendingPathComponent(Filenames.Albums)
let encoder = JSONEncoder()
guard let encodedData = try? encoder.encode(albums) else {
return
}
try? encodedData.write(to: url)
}
Here, you're defining a URL where you'll save the file (like you did with caches
), a constant for the filename, then a method which writes your albums out to the file. And you didn't have to write much code!
The other part of the process is decode back the data into a concrete object. You're going to replace that long method where you make the albums and load them from a file instead. Download and unzip this JSON file and add it to your project.
Now replace init
in PersistencyManager.swift with the following:
let savedURL = documents.appendingPathComponent(Filenames.Albums)
var data = try? Data(contentsOf: savedURL)
if data == nil, let bundleURL = Bundle.main.url(forResource: Filenames.Albums, withExtension: nil) {
data = try? Data(contentsOf: bundleURL)
}
if let albumData = data,
let decodedAlbums = try? JSONDecoder().decode([Album].self, from: albumData) {
albums = decodedAlbums
saveAlbums()
}
Now, you're loading the album data from the file in the documents directory, if it exists. If it doesn't exist, you load it from the starter file you added earlier, then immediately save it so that it's there in the documents directory next time you launch. JSONDecoder
is pretty clever - you tell it the type you're expecting the file to contain and it does all the rest of the work for you!
You may also want to save the album data every time the app goes into the background. I'm going to leave this part as a challenge for you to figure out - some of the patterns and techniques you've learned in these two tutorials will come in handy!