Migrating to Codable from NSCoding

For those who came in late: Apple introduced the `Codable` protocol in Swift 4, which allows you to encode and decode your model types to data types such as JSON and property lists, and can be used instead of the `NSCoding` protocol to archive your data.

First of all, what’s the big deal with this Codable protocol? Is it really worth the effort to learn a new approach when we already have NSCoding, that’s worked fine for years for archiving data? And if you look at a type implementing both, they are pretty similar.

Imagine we have a Product type, that contains a title, price, and quantity:

struct Product {
 var title:String
 var price:Double
 var quantity:Int
}

Simple model type using NSCoding

Let’s look first at how we would implement this type using NSCoding:

class Product: NSObject, NSCoding {
  var title:String
  var price:Double
  var quantity:Int
  enum Key:String {
    case title = "title"
    case price = "price"
    case quantity = "quantity"
  }
  init(title:String,price:Double, quantity:Int) {
   self.title = title
   self.price = price
   self.quantity = quantity
  }
  func encode(with aCoder: NSCoder) {
   aCoder.encode(title, forKey: Key.title.rawValue)
   aCoder.encode(price, forKey: Key.price.rawValue)
   aCoder.encode(quantity, forKey: Key.quantity.rawValue)
  }
  convenience required init?(coder aDecoder: NSCoder) {
   let price = aDecoder.decodeDouble(forKey: Key.price.rawValue)
   let quantity = aDecoder.decodeInteger(forKey: Key.quantity.rawValu  e)
   guard let title = aDecoder.decodeObject(forKey: Key.title.rawValue) as? String else { return nil }
   self.init(title:title,price:price,quantity:quantity)
  }
}

A few points:

  • We implemented the type as a class instead of a struct, as the type needs to subclass NSObject.
  • We created a Keys enum (or could have been a struct) with values are used to archive and unarchive the type’s properties.
  • We adopted the NSCoding protocol, and implemented its two required methods:
    • init which initializes the type based on decoded information from an NSCoder object.
    • encode which encodes the type into an NSCoder object.

Simple model type using Codable

Ok, now let’s look at how we could implement this type using Codable:

struct Product: Codable {
  var title:String
  var price:Double
  var quantity:Int
  enum CodingKeys: String, CodingKey {
    case title
    case price
    case quantity
  }
  init(title:String,price:Double, quantity:Int) {
    self.title = title
    self.price = price
    self.quantity = quantity
  }
  func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    try container.encode(title, forKey: .title)
    try container.encode(price, forKey: .price)
    try container.encode(quantity, forKey: .quantity)
  }
  init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    title = try container.decode(String.self, forKey: .title)
    price = try container.decode(Double.self, forKey: .price)
    quantity = try container.decode(Int.self, forKey: .quantity)
  }
}

Well, that looks familiar:

  • There is a CodingKeys enum, this time defined that describes the properties to encode.
  • The Product type adopts the Codable protocol, and implements its two required methods:
    • init which initializes the type based on decoded information from a Decoder object.
    • encode which encodes the type into an Encoder object.

Comparing Codable with NSCoding

Here’s a look at the similarities between the two approaches, in defining the model type:

Model type using Codable protocol vs using NSCoding protocol

Why use the Codable protocol?

So — if they’re so similar, why bother?

Well, there are a few good reasons:

  • You may have noticed that the Product type in the Codable example is a struct rather than a class — it doesn’t have to be, it’s just that as types that implement the Codable protocol don’t need to subclass (a reminder — NSCoding types must subclass NSObject, and therefore must be a class) they are free to be any sort of type — class, struct or even enum. The power!
  • Though I’m focusing on archiving to disk in this article, the Codable protocol isn’t just useful for archiving, it is also useful for encoding or decoding data to different data types. At the time of writing this includes JSON and property lists.
  • If it’s only encoding or decoding you’re after (for example, you might just be receiving and decoding data from a remote API), you can simply adopt the Encodable or Decodable protocols.
  • Anyone with experience with Codable is probably screaming about this advantage at the screen by now! So…the magic of the Codable protocol is that it has the power to automatically generate both of its required methods (encode and init) and the CodingKeys enum. If you’re after a straightforward encoding/decoding of Codable properties in your type, you can leave these out, and allow the compiler to auto-generate them, wow!

Model type using Codable protocol — manually specified vs generating automatically.

Why would you ever manually compose Codable functions?

Auto-generation is awesome, but there are still several circumstances where you will need to forego the convenience of auto-generation, and manually compose one or all of these. For example:

  • One or more properties of the type may not be Codable. In this case, you’ll need to convert it to and from a Codable type.
  • The structure of the type may differ from the structure you want to encode/decode.
  • You may want to encode and decode different properties than the properties of the type.
  • You may want to use different names for properties in the type.

Comparing archiving with Codable and NSCoding

We’ve seen how defining the model type compares, but how does the actual archiving of data compare?

Here is arching and unarchiving data the Product class with NSCoding:

func storeProducts() {
  let success = NSKeyedArchiver.archiveRootObject(products, toFile: productsFile.path)
  print(success ? "Successful save" : "Save Failed")
}
func retrieveProducts() -> [Product]? {
  return NSKeyedUnarchiver.unarchiveObject(withFile: productsFile.path) as? [Product]
}

And here is archiving and unarchiving our Product struct with Codable:

func storeProducts() {
  do {
    let data = try PropertyListEncoder().encode(products)
    let success = NSKeyedArchiver.archiveRootObject(data, toFile: productsFile.path)
    print(success ? "Successful save" : "Save Failed")
  } catch {
    print("Save Failed")
  }
}
func retrieveProducts() -> [Product]? {
  guard let data = NSKeyedUnarchiver.unarchiveObject(withFile: productsFile.path) as? Data else { return nil }
  do {
    let products = try PropertyListDecoder().decode([Product].self, from: data)
    return products
  } catch {
    print("Retrieve Failed")
    return nil
  }
}

You can see it is a little more wordy to use the Codable protocol.

This is due to two factors:

  • Encoding and decoding can throw errors with the Codable protocol. This is a good thing — it means your app is less likely to crash unexpectedly while encoding and decoding with the Codable protocol.
  • You need to manually request that your data type is encoded using a custom encoder, before passing it into be archived. (The same can be said in reverse for decoding and unarchiving.)

Migrating to Codable

So, you can see that implementing the Codable protocol vs the NSCoding protocol definitely has its benefits. The question is — what should we do with legacy projects that use NSCoding?

Well, certainly, we could do nothing. Apple hasn’t declared that they’re deprecating NSCoding, so you should be right to continue using it.

But if you do wish to update your code to the more modern Codable protocol, how…?

If we just switch our NSCoding type to a Codable type, and we run the app on a device with preexisting NSCoding data, the app won’t just fail when it tries to unarchive it as Codable data, it will crash with an NSInvalidUnarchiveOperationException. A disaster for your loyal users!

One solution is to adopt BOTH the Codable and the NSCoding protocols. The first time a legacy user uses the app, they will retrieve the data using the NSCoding protocol. Any subsequent time they save data, they will save it using the Codable protocol, and henceforth will purely be accessing their data via the Codable protocol. At some point in the distant future, you could decide to no longer support data stored using NSCoding.

Adopting both the NSCoding and Codable protocols is pretty straight-forward. Your model type that already adopts NSCoding just needs to also adopt Codable, and it will automatically synthesize Codable methods. That means, just a change to the class header: (assuming your model type doesn’t have a more complicated structure that requires manually composing Codable methods)

class Product: NSObject, Codable, NSCoding {

Retrieving data should now no longer crash if the stored data is legacy, as your Codable type also implements NSCoding and subclasses NSObject.

You will just need to update the retrieveProducts method, to handle the data regardless of whether it is encoded using NSCoding or Codable:

func retrieveProducts() -> [Product]? {
  let unarchivedData = NSKeyedUnarchiver.unarchiveObject(withFile: productsFile.path)
  //Work with Codable
  if let data = unarchivedData as? Data {
    do {
      let decoder = PropertyListDecoder()
      let products = try decoder.decode([Product].self, from: data)
      return products
    } catch {
      print("Retrieve Failed")
      return nil
    }
  }
  // Work with NSCoding
  else if let products = unarchivedData as? [Product] {
    return products
  } else {
    return nil
  }
}

As we will store the data in Codable format regardless of how we have retrieved it, the store function shouldn’t change from how it was when storing Codable data.

That’s it! Your app should now be migrated to use Codable, and deal with any legacy NSCoding data. The next time the user saves data, they will be saving it to the Codable format.

 

iOS development with Swift - book: https://manning.com/books/ios-development-with-swift video course: https://www.manning.com/livevideo/ios-development-with-swift-lv

Tagged with: , , ,
Posted in Swift
3 comments on “Migrating to Codable from NSCoding
  1. Great article. Another benefit of using Codable is the data size is also way smaller.

    • Great point, as they’re both binary data I assumed they would be pretty much the same, but from a small experiment, I am seeing NSCoding archives as around 13% bigger.

  2. […] are some nice articles (e.g. by Todd Olsen or Craig Grummitt) which show how to use the new protocols and how to migrate from the old world. I want to update my […]

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: