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:
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 forDecodable & Encodable
. So if you only want to read the property list file you can just adoptDecodable
.- 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
}