I have a love-hate relationship with property lists: although they’re quite a good universal solution for storing preferences and many other settings, there’s no good tool bundled with macOS to work with them, and accessing them in your own apps can be very frustrating.
Apple hasn’t helped by deprecating some of the great shortcuts in handling property lists other than UserDefaults. When working on LockRattler 4.1, I was frustrated to discover that older code for reading in a property list to Swift data types was now thoroughly deprecated.
A little searching showed that none of the iBooks that I have bought on Swift, even Apple’s two free books, offered any solutions. Some articles on the internet looked promising, but all seem to have been written for people who already knew what they were doing. I didn’t, and they just left me confused.
My task was to read in a property list containing details of all the software updates which have been installed, so that I could parse it to discover the latest date, etc., of certain types of update. So in traditional terms, I was starting from the file with an array of small dictionaries, like
<dict>
<key>contentType</key>
<string>config-data</string>
<key>date</key>
<date>2017-12-07T20:23:36Z</date>
<key>displayName</key>
<string>Gatekeeper Configuration Data</string>
<key>displayVersion</key>
<string>134</string>
<key>packageIdentifiers</key>
<array>
<string>com.apple.pkg.GatekeeperConfigData.16U1325</string>
</array>
<key>processName</key>
<string>softwareupdated</string>
</dict>
and wanted to end up with a Swift Array of Dictionary. Neat shortcuts like NSArray.arrayWithContentsOfFile
are now deprecated.
The Swift 4 solution, supported by its standard library, is a Coder, which can encode and decode automatically. It’s sufficiently new and exciting to have been covered in one of the sessions at WWDC 2017, so I looked at the accompanying documentation. It looks really simple, but understanding how to implement it here was rather tougher.
For this example, I will stick with my problem of decoding a property list consisting an array of dictionaries. That should provide a sound base from which you can extend the technique to encoding, and dealing with dictionaries of dictionaries, and other structured formats, even JSON rather than property list data.
First, study the property list, and turn it into a struct
rather than a dictionary. The above, for instance, would become
struct theReceipts: Decodable {
var contentType: String
var date: Date
var displayName: String
var displayVersion: String
var packageIdentifiers: Array<String>
var processName: String
}
Life isn’t always that simple, of course. Sometimes, keys are optional. In those cases, define those as optionals, as in
var contentType: String?
within the struct. You don’t have to capture the precise structure of the dictionary, so long as you capture the elements which you want to access. In my case, this boils down to
struct theReceipts: Decodable {
var date: Date
var displayName: String
var displayVersion: String
}
That struct must be placed at the top level in one of your Swift source files, typically the one in which your code will do the decoding.
Using that in a decoder is extremely simple. Here’s the skeleton code:
func getLatestInstalls() {
// first get a URL for the property list we want to read
let thePlistURL: URL = URL.init(fileURLWithPath: receiptsPath)
// then make a typealias to an array of our custom struct
typealias receipts = [theReceipts]
// and declare an optional variable of that type
var theRcpts: receipts?
// because this can throw, perform it in a try … catch … structure
do {
// first get the contents of the property list from that URL
let data = try Data(contentsOf: thePlistURL)
// create the decoder
let decoder = PropertyListDecoder()
// then decode the data into the structured array, which can also throw
theRcpts = try decoder.decode(receipts.self, from: data)
// because theRcpts is an optional, it’s time to cash it in, then iterate through it
let theRcptArr = theRcpts!
if theRcptArr.count > 0 {
for item in theRcptArr {
// now to access the individual properties, we don’t use keys but
if (item.displayName == gcdString) {
let theGCDDate = item.date
// and so on; then remember to include a functional catch
} } } } catch {
self.doErrorAlertModal(message: error.localizedDescription)
} }
That is all you have to do. Coders really are that simple to use.