iOS persistence with NSCoder and NSKeyedArchiver



As you have seen in the last post, NSUserDefaults is not as sophisticated as we want it to be in more complex scenarios such as persisting an object graph like an array of arrays, for example. Often times we get into situations where we do not see app persistence even though we should.

Let’s start with an example. When you create a Master-Detail project in Xcode you will notice that all the date entries you create will be lost if you run the app again. If we look at the code inside MasterViewController we notice that a mutable array named objects is used to store objects of the type NSDate every time we click the + button at the right end of the navigation bar. So objects will be a proper object graph candidate for us to persist.

When the app starts for the first time there is no file to store the array, so we first need to construct the full path. Let’s add a calculated Swift property to MasterViewController that specifies the location of a file name we choose, and then place it inside the Documents directory:

var filePath : String {
    let manager = NSFileManager.defaultManager()
    let url = manager.URLsForDirectory(.DocumentDirectory, inDomains: .UserDomainMask).first as! NSURL
    return url.URLByAppendingPathComponent("objectsArray").path!
}

The root of the graph for this app is the objects array. We can persist the array using a class method on the NSKeyedArchiver and add this line to the insertNewObject() method:

NSKeyedArchiver.archiveRootObject(objects, toFile: filePath)

To unarchive the array from the file we use a conditional optional check in case our file does not exist, and add it to the viewDidLoad() method:

if let array = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [AnyObject] {
    objects = array
}

Now run the app, create a few date entries, close the app and run it again. You should now see that the array is being persisted. That was easy. But what about complex objects graphs such as arrays of custom objects? In this case we need to complement NSKeyedArchiver and NSKeyedUnarchiver with the NSCoding protocol that we will need to implement.

Let’s create a new Person class:

class Person : NSObject, NSCoding {
 
    struct Keys {
        static let Name = "name"
        static let Age = "age"
    }
    
    var name = ""
    var age = 0
    
    init(dictionary: [String : AnyObject]) {
        name = dictionary[Keys.Name] as! String
        age = dictionary[Keys.Age] as! Int
    }
    
    func encodeWithCoder(archiver: NSCoder) {
        archiver.encodeObject(name, forKey: Keys.Name)
        archiver.encodeObject(age, forKey: Keys.Age)
    }

    required init(coder unarchiver: NSCoder) {
        super.init()
        name = unarchiver.decodeObjectForKey(Keys.Name) as! String
        age = unarchiver.decodeObjectForKey(Keys.Age) as! Int
    }
}

You noticed we conformed this class to the NSCoding protocol so we needed to implement the two methods the protocol requires, init(coder:) and encodeWithCoder(archiver:). Assume that we have another array persons in MasterViewController and another method that adds Person objects to this array. Having the Person class set up with an archiver for saving data and an unarchiver for retrieving saved data makes our task as easy as calling the archiver in the viewWillAppear() method:

NSKeyedArchiver.archiveRootObject(persons, toFile: filePath)

and the unarchiver in the viewDidLoad() method:

persons = NSKeyedUnarchiver.unarchiveObjectWithFile(filePath) as? [Person] ?? [Person]()

Stay tuned for the last part of this series.

Until next time!