Using NSKeyedArchiver to Persist Data (Swift 3)

Photo Credit: NHipster.com

Apple provides two ways to persist data between app launches: Core Data and NSKeyedArchiver.

NSKeyedArchiver encodes (saves) and decodes (retrieves) any NSCoding compliant classes you want persisted. While NSKeyedArchiver is not as robust as Core Data (it’s slower and manual), it accomplishes the desired job of persisting data and can be less complex than Core Data.

NSCoding is a protocol that requires two methods — required init(coder decoder: NSCoder) and encode(with coder: NSCoder). If I create a class that conforms to NSObject AND NSCoder, then my class can be serialized (translated from its current data structure into a format that can be stored as bytes) and deserialized (extracted from bytes into a data structure) into data that can be saved to a user’s disk.

To demonstrate, I will be creating a shopping list app.

Step 1: Create some UI in Storyboard.

For this app, I used a tab bar controller. The first tab, Add New Item, contained a label, a textField, and a button. The second tab, Shopping List, was just a tableView that displayed my added items from the first tab.

Step 2: Create my model object and a singleton to be my shared DataStore.

My objects will be ShoppingItems and for now, each ShoppingItem has one property: name.

I created a class called ShoppingList and adopted the NSObject and NSCoding protocols, which will allow this class to be NSKeyedArchiver compliant.

I create 1) the name property for my ShoppingList, 2) an initializer for my ShoppingList and 3) I create a struct that holds a static name property to hold my NSCoding keys (just to be safe).

NSCoder requires two methods, a required init for decoding and an encode with coder. The encode (with coder: NSCoder) method will save (encode) the name property for the Key.name I created. The required init(coder decoder: NSCoder) method will retrieve my saved name object and cast it as a string.

Line #29 — I implement the decoding method that NSKeyedArchiver will call. Line #37 — I implement the encoding method that NSKeyedArchiver will call.

I also create a getter/setter for my name property, to ensure it is updated with the newValue loaded.

I also used a singleton to serve as a “dataStore” to have one location for my shoppingList array.

I love singletons.

Step 3: Create an Outlet and Action for my textfield and button.

These are needed so that I can retrieve the entered text in the textField and save this text as a new shoppingItem once the button is tapped.

Step 4: Create a variable that represents a filePath which will specify where we should be saving this data.

In my viewController that has the textField and button, I create a filePath property. This filePath property should create a FileManager (like a file cabinet files and folders on a phone). It should also retrieve a url in the FileManager’s array of urls in our documentDirectory. Here, I retrieved the first filePath. The filePath property should return this url and append a path that you specify, I used “Data.”

var filePath: String {
   //1 - manager lets you examine contents of a files and folders in your app; creates a directory to where we are saving it
   let manager = FileManager.default
  //2 - this returns an array of urls from our documentDirectory and we take the first path
  let url = manager.urls(for: .documentDirectory, in: .userDomainMask).first
  print("this is the url path in the documentDirectory \(url)")
  //3 - creates a new path component and creates a new file called "Data" which is where we will store our Data array.
  return (url!.appendingPathComponent("Data").path)
}

Step 5: Write a function that will save this data to a user’s phone.

I write a private function that saves my shoppingItem by 1) appending to my shoppingItems array in my DataStore singleton 2) uses NSKeyedArchiver to archive this shoppingItems array to my designated file path.

private func saveData(item: ShoppingItem) {
   self.store.shoppingItems.append(item)
   //4 - NSKeyedArchiver is going to look in every shopping list class and look for encode function and is going to encode our data and save it to our file path.  This does everything for encoding and decoding.
   //5 - archive root object saves our array of shopping items (our data) to our filepath url
   NSKeyedArchiver.archiveRootObject(self.store.shoppingItems, toFile: filePath)
}

The NSKeyedArchiver explicitly calls on the encode and decode functions I wrote in my shoppingList model. The archiveRootObject function will save it to the file path that we declared earlier.

Step 6: Write a function that will load our saved data.

Once we have saved our data, we want to be able to retrieve our data after we have closed our app and want to reopen it.

To do this, we unwrap our NSKeyedArchiver and call on the unarchiveObject method which will deserialize our data into our desired ShoppingItem array. We can then take this loaded data and assign it back to our array of shoppingItems in our singleton.

private func loadData() {
   //6 - if we can get back our data from our archives (load our data), get our data along our file path and cast it as an array of ShoppingItems
   if let ourData = NSKeyedUnarchiver.unarchiveObject(withFile: filePath) as? [ShoppingItem] {
   self.store.shoppingItems = ourData
   }
}

Step 7: Run the app, add a few shoppingList items, stop the app, close the app in the simulator, open the app again to see if my items persisted.

I look forward to using this to persist some of my app data in the future!

Leave a comment below and let me know if you found this helpful.

You can find my repo here.

Resources:

NSHipster — NSCoding: http://nshipster.com/nscoding/

Apple Documentation — NSKeyedArchiver: https://developer.apple.com/reference/foundation/nskeyedarchiver