Saving Data to Show Offline
September 17, 2015

Sometimes we want to keep data between runs of our apps. NSUserDefaults is fine for a little bit of data. If there’s a lot of data then a full blown database makes sense, maybe Core Data, SQLite or Realm. But what if it’s a middling amount of data? Then use NSKeyedArchiver. It’s a lot simpler to set up than a database and can hold a lot more data than NSUserDefaults. If you don’t need fancy database features like querying then NSKeyedArchiver will save you a lot of coding time.

Today we’ll use NSKeyedArchiver to handle not having an internet connection in our Stock Quote Page View Controller demo app. We’ll do that by persisting the stock quotes each time we fetch them from the web service.

This tutorial has been updated to use Swift 2.0, Xcode 7.0, Alamofire 3.0.0, and SwiftyJSON 2.3.0.

To start we’ll grab the code from GitHub:


git clone git@github.com:cmoulton/grokRESTPageViewController.git
cd grokRESTPageViewController
git checkout alamofire-3.0

The git checkout alamofire-3.0 bit is needed to make sure we get the code as it was at the end of the last tutorial.

If you’ve been working through the previous tutorials make sure your Podfile is up to date:


source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
use_frameworks!
pod 'Alamofire', '~> 3.0.0'
pod 'SwiftyJSON', '~> 2.3.0'

Install the CocoaPods:


pod install

Open the .xcworkspace file in Xcode 7 to start coding.

Adding a Date

Since we’re going to be displaying stock quotes that might be a bit old we should save the date that they were fetched. We’ll add a property to the StockQuoteItem class for that:


class StockQuoteItem: ResponseJSONObjectSerializable {
  let symbol: String?
  let ask: String?
  let yearHigh: String?
  let yearLow: String?
  let timeSaved: NSDate?
  ...
}

We can initialize it when we create a new StockQuoteItem:


required init?(json: SwiftyJSON.JSON) {
  self.symbol = json["symbol"].string
  self.ask = json["Ask"].string
  self.yearHigh = json["YearHigh"].string
  self.yearLow = json["YearLow"].string
  self.timeSaved = NSDate()
}

Calling NSDate() will give us the current date and time.

Implement NSCoding

NSKeyedArchiver saves and loads objects & values using the NSCoding protocol. Lots of types like Array, String, and all of the numeric types already implement NSCoding so those are easy to save. In the next section we’ll sort out just how to save and load the objects. First we need to make our custom objects implement NSCoding so we don’t get crashes when we load and save.

There are 2 required functions in the NSCoding protocol:


public protocol NSCoding {    
  public func encodeWithCoder(aCoder: NSCoder)
  public init?(coder aDecoder: NSCoder)
}

NSCoder is a super class of NSKeyedArchiver and NSKeyedUnarchiver, which is why we don’t see NSKeyedArchiver or NSKeyedUnarchiver here.

The first NSCoding function is to save the object and the second method function is to re-create the object when we load it. We’ll be saving & loading our StockQuoteItem objects so we need to make that class conform to this protocol:


class StockQuoteItem: NSObject, NSCoding, ResponseJSONObjectSerializable {
  ...

  // MARK: - NSCoding
  func encodeWithCoder(aCoder: NSCoder) {
    // TODO: implement
  }
  
  required convenience init?(coder aDecoder: NSCoder) {
    // TODO: implement
  }
}

We need to add NSObject as well since NSCoding uses it behind the scenes. Maybe someday we’ll get a more Swift-y alternative.

Those two functions are used to convert our object to and from an NSCoder. To encode we can use encodeObject: forKey::


func encodeWithCoder(aCoder: NSCoder) {
  aCoder.encodeObject(self.symbol, forKey: "symbol")
  aCoder.encodeObject(self.ask, forKey: "ask")
  aCoder.encodeObject(self.yearHigh, forKey: "yearHigh")
  aCoder.encodeObject(self.yearLow, forKey: "yearLow")
  aCoder.encodeObject(self.timeSaved, forKey: "timeSaved")
}

Then to decode the object we can use decodeObjectForKey::


required convenience init?(coder aDecoder: NSCoder) {
  let symbol = aDecoder.decodeObjectForKey("symbol") as? String
  let ask = aDecoder.decodeObjectForKey("ask") as? String
  let yearHigh = aDecoder.decodeObjectForKey("yearHigh") as? String
  let yearLow = aDecoder.decodeObjectForKey("yearLow") as? String
  let timeSaved = aDecoder.decodeObjectForKey("timeSaved") as? NSDate
  // TODO: create object from decode values
}

If you have other types of properties then you can use similar functions for your type, like encodeBool: forKey: and encodeInteger: forKey:.

Now we need to create an instance at the end of the new init function. Here’s our current init:


required init?(json: SwiftyJSON.JSON) {
  self.symbol = json["symbol"].string
  self.ask = json["Ask"].string
  self.yearHigh = json["YearHigh"].string
  self.yearLow = json["YearLow"].string
  self.timeSaved = NSDate()
}

We need to create an initializer that takes each property as an argument so let’s refactor the existing one:


// MARK: Initializers
required init(symbol: String?, ask: String?, yearHigh: String?, yearLow: String?, timeSaved: NSDate?) {
  self.symbol = symbol
  self.ask = ask
  self.yearHigh = yearHigh
  self.yearLow = yearLow
  self.timeSaved = timeSaved
}
  
required convenience init?(json: SwiftyJSON.JSON) {
  self.init(symbol: json["symbol"].string, ask: json["Ask"].string, yearHigh: json["YearHigh"].string, yearLow:json["YearLow"].string, timeSaved: NSDate())
}

Then we can finish up the NSCoding protocol implementation:


required convenience init?(coder aDecoder: NSCoder) {
  let symbol = aDecoder.decodeObjectForKey("symbol") as? String
  let ask = aDecoder.decodeObjectForKey("ask") as? String
  let yearHigh = aDecoder.decodeObjectForKey("yearHigh") as? String
  let yearLow = aDecoder.decodeObjectForKey("yearLow") as? String
  let timeSaved = aDecoder.decodeObjectForKey("timeSaved") as? NSDate
  self.init(symbol: symbol, ask: ask, yearHigh: yearHigh, yearLow: yearLow, timeSaved: timeSaved)
}

Create a Persistence Manager

To keep our code organized we’ll create a new class that’s responsible for saving and loading objects. Add a PersistenceManager.swift file to your project:


import Foundation

class PersistenceManager {
}

To save objects we’ll use this NSKeyedArchiver function:


public class func archiveRootObject(rootObject: AnyObject, toFile path: String) -> Bool

And this NSKeyedUnarchiver function will load them:


public class func unarchiveObjectWithFile(path: String) -> AnyObject?

Both require a path so let’s set up our PersistenceManager to be able to create the path:


enum Path: String {
  case StockQuotes = "StockQuotes"
}

class PersistenceManager {
  class private func documentsDirectory() -> NSString {
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let documentDirectory = paths[0] as String
    return documentDirectory
  }
}

We’ll be able to get the file path like this:


let path = .StockQuotes
let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)

path.rawValue gives us the value that we set for the enum, like "StockQuotes". We’re using the enum so that we could add more objects with different paths later without polluting the code base with hard-coded file names.

The objects that we want to save and load are NSArray objects. We couldn’t use Swift arrays since they don’t have an indexOfObject function and that function made our UIPageViewController implementation a lot cleaner. So let’s set up loading and saving an NSArray:


class PersistenceManager {
  class private func documentsDirectory() -> NSString {
    let paths = NSSearchPathForDirectoriesInDomains(.DocumentDirectory, .UserDomainMask, true)
    let documentDirectory = paths[0] as String
    return documentDirectory
  }
  
  class func saveNSArray(arrayToSave: NSArray, path: Path) {
    let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
    NSKeyedArchiver.archiveRootObject(arrayToSave, toFile: file)
  }
  
  class func loadNSArray(path: Path) -> NSArray? {
    let file = documentsDirectory().stringByAppendingPathComponent(path.rawValue)
    let result = NSKeyedUnarchiver.unarchiveObjectWithFile(file)
    return result as? NSArray
  }
}

In both cases we just get the file path and then call the NSKeyedArchiver or NSKeyedUnarchiver function. When unarchiving we also cast the result back to the expected type before returning it.

We could generalize these function to accept any object that implements NSCoding using generics but we don’t need to so let’s not do that. Generics are very powerful but they can make code more challenging to read. Like most programming it’s a trade-off whether the complexity is worth the benefit. I don’t think it is here but we could always change it later.

Save and Load the Data

We have the power to archive and unarchive arrays but we need to hook that up in our code.

For archiving, we’ll do it right after we load new stock quotes. We just need to add a call to PersistenceManager.saveNSArray once we’ve saved the objects:


func loadStockQuoteItems(completionHandler: (NSError?) -> Void) {
  let symbols = ["AAPL", "GOOG", "YHOO"]
  StockQuoteItem.getFeedItems(symbols, completionHandler:{ (result) in
    if let error = result.error {
      completionHandler(error)
      return
    }
    if let stocks = result.value {
      let mutableObjects = NSMutableArray()
      for stock in stocks { // because we're getting back a Swift array but it's easier to do the PageController with an NSArray
        mutableObjects.addObject(stock)
      }
      self.dataObjects = mutableObjects
      // save them to the device
      PersistenceManager.saveNSArray(self.dataObjects, path: .StockQuotes)
    }
    // success
    completionHandler(nil)
  })
}

While working on that code I noticed that we had declared the array of StockQuoteItems as an NSMutableArray but we weren’t really adding and removing items from it. So I’ve switched it to var dataObjects = NSArray() and used a temporary let mutableObjects = NSMutableArray() to collect up the stocks from the web service before setting them to the property: self.dataObjects = mutableObjects.

Unarchiving is a little more complicated. Right now when we get an error we just call the completionHandler and return without setting a value for dataObjects:


if let error = result.error as? NSError {
  completionHandler(error)
  return
}

We need to change that to get the saved stock quotes, if we have any:


if let error = result.error {
  // show the saved values
  if let values = PersistenceManager.loadNSArray(.StockQuotes) {
    self.dataObjects = values
  }
  completionHandler(error)
  return
}

And we need to update the code that calls that function so it knows that it might have stock quotes to display, even though it got an error. Here’s what it looks like now:


dataController.loadStockQuoteItems{ (error) in
  if error != nil {
    let alert = UIAlertController(title: "Error", message: "Could not load stock quotes \(error?.localizedDescription)", preferredStyle: UIAlertControllerStyle.Alert)
    alert.addAction(UIAlertAction(title: "Click", style: UIAlertActionStyle.Default, handler: nil))
    self.presentViewController(alert, animated: true, completion: nil)
  } else {
    // set first view controller to display
    if let firstVC = self.viewControllerAtIndex(0) {
      let viewControllers = [firstVC]
      self.setViewControllers(viewControllers, direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
    }
  }
}

Before we add displaying the saved quotes let’s fix that error dialog so it doesn’t show the text like Optional(""):


dataController.loadStockQuoteItems{ (error) in
  if error != nil {
    let alert:UIAlertController
    if let description = error?.localizedDescription {
      alert = UIAlertController(title: "Error", message: "Could not load stock quotes \(description)", preferredStyle: UIAlertControllerStyle.Alert)
    } else {
      alert = UIAlertController(title: "Error", message: "Could not load stock quotes", preferredStyle: UIAlertControllerStyle.Alert)
    }
    alert.addAction(UIAlertAction(title: "Ok", style: UIAlertActionStyle.Default, handler: nil))
    self.presentViewController(alert, animated: true, completion: nil)
  } else {
    ...
  }
}

To tell the page view controller that it has data we can treat the case with an error just like the case without an error:


dataController.loadStockQuoteItems{ (error) in
  if error != nil {
    ...
  }
  // set first view controller to display
  if self.dataController.dataObjects.count > 0 {
    if let firstVC = self.viewControllerAtIndex(0) {
      let viewControllers = [firstVC]
      self.setViewControllers(viewControllers, direction: UIPageViewControllerNavigationDirection.Forward, animated: false, completion: nil)
    }
  }
}

Notice the change? We just removed the else from the if error != nil case. Now the app will try to load stock quotes as long as it has at least one stock quote in dataObjects, regardless of whether there was an error.

Update the Interface

Now that we’re potentially showing stock quotes that are out of date we should show the time they were fetched to the user. Let’s add the date to the text on each page. In SinglePageViewController, we’ll create an NSDateFormatter, use it to convert the timeSaved to a string and append it to the label’s text:


override func viewWillAppear(animated: Bool) {
  super.viewWillAppear(animated)

  if let aSymbol = stock?.symbol, anAsk = stock?.ask, high = stock?.yearHigh, low = stock?.yearLow {
    dataLabel.text = aSymbol + ": " + anAsk + "\n" + "Year High: " + high + "\n" + "Year Low: " + low
  
    let dateFormatter = NSDateFormatter()
    dateFormatter.dateStyle = .ShortStyle
    dateFormatter.timeStyle = .ShortStyle
  
    if let date = stock?.timeSaved {
      let dateAsString = dateFormatter.stringFromDate(date)
      dataLabel.text = dataLabel.text! + "\n" + "Loaded: " + dateAsString
    }
  } else {
    dataLabel.text = stock?.symbol
  }
}

Nothing too fancy. You might need to open your storyboard and change the label’s Autoshrink setting so that the text can get smaller to fit it all in:

Set autoshrink setting in Xcode

Save and run to test it out. Let me know if you find any bugs :)

And That’s All

That’s all it takes to load and save objects using NSKeyedArchiver:

  • Implement NSCoding on your custom classes
  • Use NSKeyedArchiver and NSKeyedUnarchiver to read and write to a file
  • Hook up the saving and loading where you want them (maybe around a web service call to allow read-only offline mode)
  • Adjust your UI if needed

Here’s the final code: grokRESTPageViewController tagged persistence.

If you’d like more Swift tutorials on topics like this one, sign up below to get them sent directly to your inbox.