NSCoding / NSKeyedArchiver
Among the most important architectural decisions made when building an app is how to persist data between launches. The question of how, exactly, to re-create the state of the app from the time it was last opened; of how to describe the object graph in such a way that it can be flawlessly reconstructed next time.
On iOS and OS X, Apple provides two options: Core Data or NSKeyed
/ NSKeyed
(which serializes <NSCoding>
-compliant classes to and from a data representation).
Or rather: three, if you include
NSURLCache
. In the case of a client-server application, having the client load necessary data on each launch is a viable design, especially when combined with a disk-based cache, which allows stored server responses to be returned immediately from matching requests. In practice, some combination of network and object caching is advisable.
When it comes to modeling, querying, traversing and persisting complex object graphs, there is no substitute for Core Data. Core Data is a big hammer, but not every problem is a nail—much less a sufficiently large nail.
A fair and common comparison of Core Data to NSKeyed
might go something like this:
Core Data | NSKeyedArchiver | |
---|---|---|
Entity Modeling | Yes | No |
Querying | Yes | No |
Speed | Fast | Slow |
Serialization Format | SQLite, XML, or NSData | NSData |
Migrations | Automatic | Manual |
Undo Manager | Automatic | Manual |
Et cetera. In a heads-up, apples to apples comparison, it looks rather one-sided.
…that is, until you look at it from a slightly different perspective:
Core Data | NSKeyedArchiver | |
---|---|---|
Persists State | Yes | Yes |
Pain in the Ass | Yes | No |
By these measures, NSKeyed
becomes a perfectly reasonable choice in certain situations. Not all apps need to query data. Not all apps need automatic migrations. Not all apps work with large or complex object graphs. And even apps that do may have certain components better served by a simpler solution.
This article will look at the how’s, when’s, and why’s of NSKeyed
and NSCoding
. And with this understanding, hopefully provide you, dear reader, with the wisdom to choose the best tool for the job.
NSCoding
is a simple protocol, with two methods: -init
and encode
. Classes that conform to NSCoding
can be serialized and deserialized into data that can be either be archived to disk or distributed across a network.
For example:
class Book: NSObject, NSCoding {
var title: String
var author: String
var page Count: Int
var categories: [String]
var available: Bool
// Memberwise initializer
init(title: String, author: String, page Count: Int, categories: [String], available: Bool) {
self.title = title
self.author = author
self.page Count = page Count
self.categories = categories
self.available = available
}
// MARK: NSCoding
required convenience init?(coder decoder: NSCoder) {
guard let title = decoder.decode Object For Key("title") as? String,
let author = decoder.decode Object For Key("author") as? String,
let categories = decoder.decode Object For Key("categories") as? [String]
else { return nil }
self.init(
title: title,
author: author,
page Count: decoder.decode Integer For Key("page Count"),
categories: categories,
available: decoder.decode Bool For Key("available")
)
}
func encode With Coder(coder: NSCoder) {
coder.encode Object(self.title, for Key: "title")
coder.encode Object(self.author, for Key: "author")
coder.encode Int(Int32(self.page Count), for Key: "page Count")
coder.encode Object(self.categories, for Key: "categories")
coder.encode Bool(self.available, for Key: "available")
}
}
@interface Book : NSObject <NSCoding>
@property NSString *title;
@property NSString *author;
@property NSUInteger page Count;
@property NSSet *categories;
@property (getter = is Available) BOOL available;
@end
@implementation Book
#pragma mark - NSCoding
- (id)init With Coder:(NSCoder *)decoder {
self = [super init];
if (!self) {
return nil;
}
self.title = [decoder decode Object For Key:@"title"];
self.author = [decoder decode Object For Key:@"author"];
self.page Count = [decoder decode Integer For Key:@"page Count"];
self.categories = [decoder decode Object For Key:@"categories"];
self.available = [decoder decode Bool For Key:@"available"];
return self;
}
- (void)encode With Coder:(NSCoder *)encoder {
[encoder encode Object:self.title for Key:@"title"];
[encoder encode Object:self.author for Key:@"author"];
[encoder encode Integer:self.page Count for Key:@"page Count"];
[encoder encode Object:self.categories for Key:@"categories"];
[encoder encode Bool:[self is Available] for Key:@"available"];
}
@end
As you can see, NSCoding
is mostly boilerplate. Each property is encoded or decoded as an object or type, using the name of the property of as the key each time. (Some developers prefer to define NSString *
constants for each keypath, but this is usually unnecessary).
But boilerplate can be a good thing sometimes—with direct control over the entire serialization process, it remains flexible to account for things like:
- Migrations: If a data model changes—such as adding, renaming, or removing a field—it should maintain compatibility with data serialized in the old format. Apple provides some guidelines on how to go about this in “Forward and Backward Compatibility for Keyed Archives”.
-
Archiving non-
NSCoding
-compatible Classes: According to object-oriented design, objects should take responsibility for encoding and decoding to and from a serialization format. However, when a class doesn’t come withNSCoding
support built in, it may be left up to class that uses it to help out.
One library that aims to cut down the boilerplate of NSCoding is Mantle, from the good folks over at GitHub. If you’re looking for more of the conveniences of Core Data modeling with
NSCoding
, Mantle is definitely worth a look.
Of course, serialization is only one part of the story. Determining where this data will persist is another question. Again, there are two approaches: writing to the local file system and using NSUser
.
File System
NSKeyed
and NSKeyed
provide a convenient API to read / write objects directly to / from disk.
An NSCoding
-backed table view controller might, for instance, set its collection property from the file manager
Archiving
NSKeyed Archiver.archive Root Object(books, to File: "/path/to/archive")
[NSKeyed Archiver archive Root Object:books to File:@"/path/to/archive"];
Unarchiving
guard let books = NSKeyed Unarchiver.unarchive Object With File("/path/to/archive") as? [Book] else { return nil }
[NSKeyed Unarchiver unarchive Object With File:@"/path/to/archive"];
NSUser Defaults
Each app has its own database of user preferences, which can store and retrieve any NSCoding
-compatible object or C value.
While it is not advisable to store an entire object graph into NSUser
, it can be useful to encode compound objects in this way, such as “current user” objects or API credentials (use Keychain instead).
Archiving
let data = NSKeyed Archiver.archived Data With Root Object(books)
NSUser Defaults.standard User Defaults().set Object(data, for Key: "books")
NSData *data = [NSKeyed Archiver archived Data With Root Object:books];
[[NSUser Defaults standard User Defaults] set Object:data for Key:@"books"];
Unarchiving
if let data = NSUser Defaults.standard User Defaults().object For Key("books") as? NSData {
let books = NSKeyed Unarchiver.unarchive Object With Data(data)
}
NSData *data = [[NSUser Defaults standard User Defaults] object For Key:@"books"];
NSArray *books = [NSKeyed Unarchiver unarchive Object With Data:data];
As developers, it is our responsibility to understand the goals and needs of our applications, and to resist the urge to over-engineer and prematurely optimize our solutions.
The decision to use Core Data in an application may appear to be a no-brainer, if not harmless. But in many cases, Core Data is discovered to be so unwieldy or unnecessary as to become a real hindrance to making something useful, let alone functional.
And even if most applications would benefit from Core Data at some point, there is wisdom to letting complexity evolve from a simple as necessary. And as far as persistence goes, it doesn’t get much simpler than NSCoding
.