You can use NSCoding
and NSKeyedArchiver
to save and load simple data objects with Swift. It’s perfect for scenarios when you don’t need a more complex tool, like Core Data or Realm.
In this tutorial, we’ll discuss:
NSKeyedArchiver
NSCoding
is an important component in iOS developmentNSKeyedArchiver
compares to components like Codable
Ready? Let’s go.
You use the NSKeyedArchiver
, and it’s sibling NSKeyedUnarchiver
, to serialize Swift data objects, like classes, in an architecture-independent binary file. You can also deserialize them to read the data back.
The NSKeyedArchiver
is closely related to components like Codable, plists, User Defaults and even JSON. They’re all components that help you transform Swift’s native classes to something you can store in a file or transmit over the internet. When you think about it, all an app does is store/send data (and give access with a UI).
The name “NSKeyedArchiver” already tells you a bit about what it does:
.zip
file but without the compression.The NSKeyedArchiver
component makes use of the NSCoding
protocol. It’s similar to Codable
, in the sense that, when your class conforms to NSCoding
, it’s suitable for use with other components that also rely on NSCoding
, like NSKeyedArchiver
. It’s a go-between that structures the data.
Let’s dive into saving and loading data objects with NSKeyedArchiver
and NSCoding
! We’re going to work with the following Swift class:
class Book {
var title:String
var author:String
var published:Int
init(title: String, author: String, published: Int) {
self.title = title
self.author = author
self.published = published
}
}
We’ll load and save the following array of books:
let books = [
Book(title: "Nineteen Eighty-Four: A Novel", author: "George Orwell", published: 1949),
Book(title: "Brave New World", author: "Aldous Huxley", published: 1932),
Book(title: "Mona Lisa Overdrive", author: "William Gibson", published: 1988),
Book(title: "Ready Player One", author: "Ernest Cline", published: 2011),
Book(title: "Red Rising", author: "Pierce Brown", published: 2014)
]
Let’s get a move on!
Before we can save and load data with NSKeyedArchiver
, our Book
class needs to adopt the NSCoding
protocol. This protocol formalizes the way custom classes, like Book
, can be serialized and deserialized.
What’s serialization anyway? It’s simple: when you serialize an object, you convert it from a Swift-only data format to a stream (or “series”) of bytes. The format-agnostic bytes can then be transmitted as ordinary data, like a string of text. At the other end, you deserialize again and recreate the original, native data format.
The NSCoding
protocol formalizes this process, so the result is always the same. We’ll need to adopt the init(coder:)
initializer, which will construct a Swift object by decoding it, and the encode(with:)
function, which will encode a Swift object.
In the previous section, we’ve already created the Book
class and given it a memberwise initializer. We’ve also created an array books
that contains a few Book
objects.
Let’s continue by adding the NSCoding
protocol to the class definition, like this:
class Book: NSObject, NSCoding {
···
}
This protocol now indicates that Book
adopts the NSCoding
protocol. It also subclasses NSObject
, a requirement to use NSCoding
.
We now need to add the 2 required functions – as per the protocol – to the class:
required convenience init?(coder: NSCoder) {
···
}
func encode(with coder: NSCoder) {
···
}
Quick Tip: Xcode can automatically add the above function declarations to your code. First, add NSCoding
to the class definition. Then, when an error appears, click the question mark in the editor, and then click Fix below “Do you want to add protocol stubs?” Neat!
We’ll start with the failable, required, convenience initializer init(coder:)
. This initializer will be able to construct Book
objects from the NSCoder
instance that’s passed to it. This is the decoding/deserialization step.
Change the init(coder:)
function to reflect the following:
required convenience init?(coder: NSCoder)
{
guard let title = coder.decodeObject(forKey: "title") as? String,
let author = coder.decodeObject(forKey: "author") as? String
else { return nil }
self.init(
title: title,
author: author,
published: coder.decodeInteger(forKey: "published")
)
}
What’s going on here? 3 parts are important:
nil
nil
, and optional type casting ensures they’re the expected typeself.init(···)
call initializes a Book
object using the memberwise initializer we defined earlierIn short, the init(coder:)
creates a Book
object by retrieving data from the coder
object. It’ll decode this data and use it to set the properties of a new Book
instance.
You can learn more about specific techniques and syntax, i.e. guard let
, by clicking any of the above links. See how far the rabbit hole goes!
Alright, now that we’ve got the decoder set up, let’s write the encoding function encode(with:)
. Make sure that your encode(with:)
function reflects the following:
func encode(with coder: NSCoder) {
coder.encode(title, forKey: "title")
coder.encode(author, forKey: "author")
coder.encode(published, forKey: "published")
}
Easy-peasy! This function encodes the properties of the Book
class. Here’s how that works:
NSKeyedArchiver
at some point, which we pass a Book
objectNSCoding
, which is greatencode(with:)
on the Book
object, and passes an instance of NSCoder
to itencode(_:forKey:)
a few times on that “coder” object, so the NSCoding
component now knows what data we want to encodeYou may be thrown off here by the lack of a return
statement in the encode(with:)
function. How can it pass data to the coder if it’s not returned by the function?
The only logical explanation here is that the NSCoder
object is passed-by-reference into the function (as a class). We’re changing the actual coder
object that’s passed into the function, so beyond this function call, those changes persists, because it’s one and the same object.
Neat! We now have a Book
class that’s ready to be encoded and decoded with NSCoding
, and any component that uses it for serialization, like NSKeyedArchiver
. Moving on…
The NSKeyedArchiver
and NSKeyedUnarchiver
components serialize objects that support NSCoding
, which you can then write to disk as a simple binary file. At a later point, you can deserialize that same file, use it in your app, make some changes, and write it back. This makes NSKeyedArchiver a simple database tool.
You can archive data, i.e. Swift object to bytes, like this:
let data = try NSKeyedArchiver.archivedData(withRootObject: books, requiringSecureCoding: false)
And you can unarchive data, i.e. bytes to Swift objects, like this:
let books = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
There’s a little more to it, though! You can already see it in the above code: NSKeyedArchiver
uses requires a so-called root object. You can’t just encode a random bunch of objects; they need to be part of one overarching, root object. In the examples in this tutorial, we’re working with one array of Book
objects.
The archiver and unarchiver makes use of the Swift Data
type. It’s a wrapper around simple byte buffers, i.e. a bunch of bytes in serial. Whenever you’re working with bytes directly in Swift, you’ll use the Data
type.
You can write Data
to a file on disk, i.e. a file in your iOS app’s documents directory, and read from it, too. Because Swift apps are sandboxed, we can’t just write to any folder on the iPhone. We’ll need to get a handle on a file in the app’s document directory.
Here’s how you do that:
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("books")
In the above code, path
contains a URL
object with a reference to a "books"
file on the iPhone’s disk. The FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
code returns a reference to the app’s document directory, which is one of the locations where we’re allowed to read/write our own files. This file has no extension.
OK, now that we’ve got a file path, let’s write the archived data to it:
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("books")
do {
let data = try NSKeyedArchiver.archivedData(withRootObject: books, requiringSecureCoding: false)
try data.write(to: path)
} catch {
print("ERROR: \(error.localizedDescription)")
}
Here’s what happens in the above code:
books
array, turning it into a Data
objectdata
object is written to the file at path
What’s awesome is that, now that the books
array is persisted to disk, we can actually load that and not use the hard-coded books
array anymore. Here’s how:
let path = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0].appendingPathComponent("books")
do {
let data = try Data(contentsOf: path)
if let books = try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? [Book] {
print(books)
}
} catch {
print("ERROR: \(error.localizedDescription)")
}
Here’s what happens in the code:
do-try-catch
block, we’re first reading the data from the file and assign it to data
, of type Data
– the bytesNSKeyedUnarchiver
we’re unarchiving the root object, i.e. the array of books, and assign it to books
[Book]
to ensure we’re getting an array of the right type instead of Any?catch
blockAwesome! We can now print out the books using a simple for loop. Like this:
for book in books {
print("\(book.title) -- \(book.author)")
}
// Nineteen Eighty-Four: A Novel -- George Orwell
// Brave New World -- Aldous Huxley
// ···
At this point, we could append another book to the books
array, and by the same route as before, save the books back to the file with NSKeyedArchiver
. This makes NSKeyedArchiver / NSKeyedUnarchiver / NSCoding a legit, simple database tool. Awesome!
In this tutorial, we’ve discussed how you can save and load simple Swift objects with NSKeyedArchiver
.
We’ve looked at how to implement NSCoding
and why, and how you can read from and write to files on disk. We’ve tied it all together by archiving an array of Book
objects, and subsequently unarchiving it again. Neat!
Here’s a quick summary for loading and saving data with NSKeyedArchiver
.
Preparation
First, make sure that your Swift class implements the NSCoding
protocol and subclasses NSObject
. Then, get a reference to a file on disk with FileManager
.
Loading data
let data = Data(contentsOf: ···)
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
Saving data
NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)
data.write(to: ···)
Want to learn more? Check out these resources: