Using Swift Codable With Property Lists

Swift 4 and iOS 11 brings us the Codable protocol as a way to convert a type to and from an external format. The most popular format may be JSON but it also works great for old school Cocoa property lists.

NSDictionary and NSArray

While I was playing with custom fonts and dynamic type last week I wanted to read a dictionary of settings from a Cocoa property list file. In the old days I might have written something like this in Objective-C:

NSURL *settingsURL = ... // location of plist file
NSDictionary *settings = [NSDictionary dictionaryWithContentsOfURL:settingsURL];

To write the dictionary back to the property list file:

[settings writeToURL:settingsURL atomically:YES];

A similar API exists for reading and writing an NSArray. Both API’s are usable from Swift with some ugly casting between NSDictionary and Dictionary (or NSArray and Array):

let settingsURL: URL = ... // location of plist file
let settings = NSDictionary(contentsOf: settingsURL) as? Dictionary<String, AnyObject>

To write the dictionary to a file you need to cast to NSDictionary:

(settings as NSDictionary).write(to: settingsURL, atomically: true)

This works but does not feel Swift like with all those casts and use of AnyObject.

Using Codable For Property Lists

The Codable protocol is new to Swift in iOS 11 and handles converting a type to and from an external format such as JSON. It also works great for property lists. In fact Apple will deprecate the old NSDictionary and NSArray methods in a future release.

Reading A Property List

For example, a property list which is a dictionary containing a boolean, string and integer:

Property List

Define a structure that matches this format and adopt the Codable protocol:

struct MySettings: Codable {
  var someFlag: Bool
  var someString: String
  var someInt: Int
}

Notes:

  • The property names match the key names of the items in the property list.
  • Codable is a typealias for Decodable & Encodable. So if you only want to read the property list file you can just adopt Decodable.
  • A property list can contain dictionaries, arrays, strings, numbers, dates, binary data and boolean values.

To read/decode the property list file create a PropertyListDecoder object and then use its decode method:

let settingsURL: URL = ... // location of plist file
var settings: MySettings?

if let data = try? Data(contentsOf: settingsURL) {
  let decoder = PropertyListDecoder()
  settings = try? decoder.decode(MySettings.self, from: data)
}

If you want to handle the errors use a do…catch block:

do {
    let data = try Data(contentsOf: settingsURL)
    let decoder = PropertyListDecoder()
    settings = try decoder.decode(MySettings.self, from: data)
} catch {
    // Handle error
    print(error)
}

If instead of a dictionary the property list file had an array at the top level:

typealias Settings = [MySettings]
var settings: Settings?
...
    settings = try decoder.decode(Settings.self, from: data)

To rename properties if you don’t want to use the key names from the property list file add a CodingKeys enum to the type. For example, to use id instead of someInt:

struct MySettings: Codable {
  var someFlag: Bool
  var someString: String
  var id: Int

  private enum CodingKeys: String, CodingKey {
    case someFlag
    case someString
    case id = "someInt"
  }
}

As simple as that. For a more complex example using a dictionary of dictionaries see the ScaledFont project from last week.

Writing A Property List

Writing or encoding a property list is just as easy. Create a PropertyListEncoder, set the output format and then use its encode method:

let someSettings = MySettings(someFlag: true, someString: "Apple", someInt: 42)
let encoder = PropertyListEncoder()
encoder.outputFormat = .xml
do {
  let data = try encoder.encode(someSettings)
  try data.write(to: settingsURL)
} catch {
  // Handle error
  print(error)
}

Note:

  • The outputFormat can be .binary, .openStep or .xml.

Objective-C

If you are using Objective-C to read and write property lists you should be aware that Apple will in a future release also deprecate these methods on NSDictionary and the similar NSArray methods:

+ dictionaryWithContentsOfFile:
+ dictionaryWithContentsOfURL:
- initWithContentsOfFile:
- initWithContentsOfURL:
- writeToFile:atomically:
- writeToURL:atomically:(BOOL)atomically

Foundation adds new methods to iOS 11 that include an error parameter:

+ dictionaryWithContentsOfURL:error:
- dictionaryWithContentsOfURL:error:
- writeToURL:error:

So to read the property list with iOS 11:

if (@available(iOS 11.0, *)) {
  NSError *error = nil;
  settings = [NSDictionary dictionaryWithContentsOfURL:settingsURL error:&error];
} else {
  // fallback to old method
  settings = [NSDictionary dictionaryWithContentsOfURL:settingsURL];
}

To write the property list with iOS 11:

if (@available(iOS 11.0, *)) {
  NSError *error = nil;
  [settings writeToURL:settingsURL error:&error];
  // Handle error
} else {
  // fallback to old method
  BOOL success = [settings writeToURL:saveURL atomically:YES];
  // Handle error
}

Further Reading