Storing Data with NSCoding and NSKeyedArchiver

Written by Reinder de Vries on September 27 2020 in App Development, iOS, Swift

Storing Data with NSCoding and NSKeyedArchiver

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:

  • How to store and retrieve data objects with NSKeyedArchiver
  • Why NSCoding is an important component in iOS development
  • How NSKeyedArchiver compares to components like Codable
  • Essential app development principles like serialization
  • A whole lot of fun code examples for you to play with

Ready? Let’s go.

  1. What’s NSKeyedArchiver?
  2. Adopting NSCoding in Swift
  3. Working with NSKeyedArchiver
  4. Saving and Archiving Data
  5. Loading and Unarchiving Data
  6. Further Reading

What’s NSKeyedArchiver?

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:

  • You’ve got “NS”, which stands for NeXTSTEP – a hint to iOS’s past – and a designation of Objective-C components.
  • Then, “Keyed” means that we’re going to store and retrieve data by key, kinda like the key-value pairs you add to a Swift dictionary.
  • Finally, “Archiver” and “Unarchiver” explains what the component does: archiving a bunch of data as a whole, like a .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!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Adopting NSCoding in Swift

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!

Coding “init(coder:)”

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:

  1. The function is a required convenience initializer, so subclasses must override it, it’s not the designated (but convenience) initializer, and it’s failable, i.e. it can return nil
  2. guard let ensures that the decoded objects aren’t nil, and optional type casting ensures they’re the expected type
  3. The self.init(···) call initializes a Book object using the memberwise initializer we defined earlier

In 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!

Coding “encode(with:)”

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:

  1. We’re using NSKeyedArchiver at some point, which we pass a Book object
  2. The archiver sees we’re conforming to NSCoding, which is great
  3. It calls encode(with:) on the Book object, and passes an instance of NSCoder to it
  4. Our function calls encode(_:forKey:) a few times on that “coder” object, so the NSCoding component now knows what data we want to encode

You 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…

Working with NSKeyedArchiver

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.

Getting the File Path

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.

Saving and Archiving Data

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:

  1. We’re getting the URL of the book’s file in the app’s document directory
  2. Within the do-try-catch block, we’re first archiving the books array, turning it into a Data object
  3. The data object is written to the file at path
  4. When an error occurs, its description is printed out to the Console

Loading and Unarchiving Data

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:

  1. Just as before, we’re getting the URL of the book’s file in the app’s document directory
  2. Within the do-try-catch block, we’re first reading the data from the file and assign it to data, of type Data – the bytes
  3. With NSKeyedUnarchiver we’re unarchiving the root object, i.e. the array of books, and assign it to books
  4. We’re also using conditional binding and type casting to [Book] to ensure we’re getting an array of the right type instead of Any?
  5. Just as before, errors are handled and shown inside the catch block

Awesome! 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!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

Further Reading

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

  1. Read the bytes from the file with let data = Data(contentsOf: ···)
  2. Get the objects with NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)

Saving data

  1. Convert objects to data with NSKeyedArchiver.archivedData(withRootObject:requiringSecureCoding:)
  2. Save data to file with data.write(to: ···)

Want to learn more? Check out these resources:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy