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 3 of 4 of this article. Click here to view the first page.

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:

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

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!