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

Add the following two methods to ViewController.swift:
//MARK: Memento Pattern
func saveCurrentState() {
  // When the user leaves the app and then comes back again, he wants 
  //it to be in the exact same state
  // he left it. In order to do this we need to save the currently displayed album.
  // Since it's only one piece of information we can use NSUserDefaults.
  NSUserDefaults.standardUserDefaults().setInteger(currentAlbumIndex, forKey: "currentAlbumIndex")
}
 
func loadPreviousState() {
  currentAlbumIndex = NSUserDefaults.standardUserDefaults().integerForKey("currentAlbumIndex")
  showDataForAlbum(currentAlbumIndex)
}
saveCurrentState saves the current album index to NSUserDefaults – NSUserDefaults is a standard data store provided by iOS for saving application specific settings and data.
loadPreviousState loads the previously saved index. This isn’t quite the full implementation of the Memento pattern, but you’re getting there.
Now, Add the following line to viewDidLoad in ViewController.swift before the scroller.delegate = self:
loadPreviousState()
That loads the previously saved state when the app starts. But where do you save the current state of the app for loading from? You’ll use Notifications to do this. iOS sends a UIApplicationDidEnterBackgroundNotification notification when the app enters the background. You can use this notification to call saveCurrentState. Isn’t that convenient?
Add the following line to the end of viewDidLoad:
NSNotificationCenter.defaultCenter().addObserver(self, selector:"saveCurrentState", name: UIApplicationDidEnterBackgroundNotification, object: nil)
Now, when the app is about to enter the background, the ViewController will automatically save the current state by calling saveCurrentState.
As always, you’ll need to un-register for notifications. Add the following code to the class:
deinit {
  NSNotificationCenter.defaultCenter().removeObserver(self)
}
This ensures you remove the class as an observer when the ViewController is deallocated.
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 centered:
swiftDesignPattern15
It looks like the album data is correct, but the scroller isn’t centered on the correct album. What gives?
This is what the optional method initialViewIndexForHorizontalScroller was meant for! Since that method’s not implemented in the delegate, ViewController in this case, the initial view is always set to the first view.
To fix that, add the following code to ViewController.swift:
func initialViewIndex(scroller: HorizontalScroller) -> Int {
  return currentAlbumIndex
}
Now the HorizontalScroller first view is set to whatever album is indicated by currentAlbumIndex. This is a great way to make sure the app experience remains personal and resumable.
Run your app again. Scroll to an album as before, put the app in the background, stop the app, then relaunch to make sure the problem is fixed:
swiftDesignPattern16
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 Albuminstances 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 the archiving mechanism.

Archiving

One of Apple’s specialized implementations of the Memento pattern is Archiving. This converts an object into a stream that can be saved and later restored without exposing private properties to external classes. You can read more about this functionality in Chapter 16 of the iOS 6 by Tutorials book. Or in Apple’s Archives and Serializations Programming Guide.

How to Use Archiving

First, you need to declare that Album can be archived by conforming to the NSCoding protocol. Open Album.swift and change the class line as follows:
class Album: NSObject, NSCoding {
Add the following two methods to Album.swift:
required init(coder decoder: NSCoder) {
  super.init()
  self.title = decoder.decodeObjectForKey("title") as! String
  self.artist = decoder.decodeObjectForKey("artist") as! String
  self.genre = decoder.decodeObjectForKey("genre") as! String
  self.coverUrl = decoder.decodeObjectForKey("cover_url") as! String
  self.year = decoder.decodeObjectForKey("year") as! String
}
 
 
func encodeWithCoder(aCoder: NSCoder) {
  aCoder.encodeObject(title, forKey: "title")
  aCoder.encodeObject(artist, forKey: "artist")
  aCoder.encodeObject(genre, forKey: "genre")
  aCoder.encodeObject(coverUrl, forKey: "cover_url")
  aCoder.encodeObject(year, forKey: "year")
}
As part of the NSCoding protocol, encodeWithCoder will be called when you ask for an Album instance to be archived. Conversely, the init(coder:) initializer will be used to reconstruct or unarchive from a saved instance. It’s simple, yet powerful.
Now that the Album class can be archived, add the code that actually saves and loads the list of albums.
Add the following method to PersistencyManager.swift:
func saveAlbums() {
  var filename = NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")
  let data = NSKeyedArchiver.archivedDataWithRootObject(albums)
  data.writeToFile(filename, atomically: true)
}
This will be the method that’s called to save the albums. NSKeyedArchiver archives the album array into a file called albums.bin.
When you archive an object which contains other objects, the archiver automatically tries to recursively archive the child objects and any child objects of the children and so on. In this instance, the archival starts with albums, which is an array of Album instances. Since Array and Album both support the NSCopying interface, everything in the array is automatically archived.
Now replace init in PersistencyManager.swift with the following code:
override init() {
  super.init()
  if let data = NSData(contentsOfFile: NSHomeDirectory().stringByAppendingString("/Documents/albums.bin")) {
    let unarchiveAlbums = NSKeyedUnarchiver.unarchiveObjectWithData(data) as! [Album]?
      if let unwrappedAlbum = unarchiveAlbums {
        albums = unwrappedAlbum
      }
  } else {
    createPlaceholderAlbum()
  }
}
 
func createPlaceholderAlbum() {
  //Dummy list of albums
  let album1 = Album(title: "Best of Bowie",
       artist: "David Bowie",
       genre: "Pop",
       coverUrl: "http://www.coversproject.com/static/thumbs/album/album_david%20bowie_best%20of%20bowie.png",
       year: "1992")
 
let album2 = Album(title: "It's My Life",
     artist: "No Doubt",
     genre: "Pop",
     coverUrl: "http://www.coversproject.com/static/thumbs/album/album_no%20doubt_its%20my%20life%20%20bathwater.png",
     year: "2003")
 
let album3 = Album(title: "Nothing Like The Sun",
            artist: "Sting",
     genre: "Pop",
     coverUrl: "http://www.coversproject.com/static/thumbs/album/album_sting_nothing%20like%20the%20sun.png",
     year: "1999")
 
let album4 = Album(title: "Staring at the Sun",
     artist: "U2",
     genre: "Pop",
     coverUrl: "http://www.coversproject.com/static/thumbs/album/album_u2_staring%20at%20the%20sun.png",
     year: "2000")
 
let album5 = Album(title: "American Pie",
     artist: "Madonna",
     genre: "Pop",
     coverUrl: "http://www.coversproject.com/static/thumbs/album/album_madonna_american%20pie.png",
     year: "2000")
  albums = [album1, album2, album3, album4, album5]
  saveAlbums()
}
You have moved the placeholder album creation code into a separate method createPlaceholderAlbum() for readability. In the new code, NSKeyedUnarchiver loads the album data from the file, if it exists. If it doesn’t exist, it creates the album data and immediately saves it for the next launch of the app.
You’ll also want to save the album data every time the app goes into the background. This might not seem necessary now but what if you later add the option to change album data? Then you’d want this to ensure that all your changes are saved.
Since the main application accesses all services via LibraryAPI, this is how the application will let PersistencyManager know that it needs to save album data.
Now add the method implementation to LibraryAPI.swift:
func saveAlbums() {
  persistencyManager.saveAlbums()
}
This code simply passes on a call to LibraryAPI to save the albums on to PersistencyMangaer.
Add the following code to the end of saveCurrentState in ViewController.swift:
LibraryAPI.sharedInstance.saveAlbums()
And the above code uses LibraryAPI to trigger the saving of album data whenever the ViewController saves its state.
Build your app to check that everything compiles.
Unfortunately, there’s no easy way to check if the data persistency is correct though. You can check the simulator Documents folder for your app in Finder to see that the album data file is created but in order to see any other changes you’d have to add in the ability to change album data.
But instead of changing data, what if you added an option to delete albums you no longer want in your library? Additionally, wouldn’t it be nice to have an undo option if you delete an album by mistake?

Comments


  1. I agree so much. we should all be reinforcing positive feedback within the comment sections. So many good points to take into consideration.
    When people have good things to say about my web site Prasoon Kumar Arya it really makes positive impact.

    ReplyDelete

Post a Comment

Popular posts from this blog

iOS Architecture

Performance Tips for IOS Application

setNeedsLayout vs layoutIfNeeded Explained