Beyond Scripting in Swift: JSON and the Golden NSDictionary

I have been working on Consolation 2 again. I first looked at what would be needed to support printing of the log excerpts from Consolation, and decided that, for this version at least, the effort involved outweighed its usefulness. The app saves log excerpts as text, and even TextEdit does a decent job of letting you print text files out. I can’t see anyone wanting to print ten thousand log entries anyway, and the burden of opening and formatting saved text using a dedicated text editor is small.

There are two outstanding tasks which I do want to complete for the version 2 release: a proper help file, and export of JSON data in a format which is usable in another app such as a spreadsheet or database.

I won’t say anything more about help files, other than they’re a bit of a mess. There isn’t a good all-round help authoring system in Xcode, so I am unsure whether to use the expensive Help Crafter, which I have just bought from the App Store, or brew myself something using Tinderbox 7. I’ll probably end up doing both, just for the experience (and being able to share it here).

JSON data is now commonplace, and there is support in macOS (and iOS) for converting it into usable data objects. Because I want to unwrap it in very particular ways, the standard tutorials seem to fall short, and – apart from Apple’s brief coverage here – invariably recommend third-party libraries such as SwiftyJSON and/or Gloss.

I have nothing against third party libraries, particularly when they’re open source as those are, but my first question was whether I need them, or I can accomplish everything using macOS. So today was Swift Playground time, the major challenge being to write code which didn’t either throw an error or crash altogether. I have learned that Xcode 8.2.1’s Playgrounds are far from being robust, at least in the face of my stumbling around in the dark.

Working in a Playground, the first hurdle was to create simple JSON data which would model the output from log show. This has two important characteristics: every log entry has its own object in an array of potentially great size, and within each object not all attributes are used. For example, greatly abbreviating a log excerpt might read:
[{
"category" : "main",
"processImageUUID" : "2813C757-9936-3ED8-830F-754F939DEEAE",


"processImagePath" : "\/System\/Library\/PrivateFrameworks\/UserActivity.framework\/Agents\/useractivityd",
"senderImagePath" : "\/System\/Library\/PrivateFrameworks\/UserActivity.framework\/Agents\/useractivityd"
},{
"processImageUUID" : "4A935997-6501-32E3-ABD8-45C501191D71",


"processImagePath" : "\/System\/Library\/Frameworks\/CoreTelephony.framework\/Support\/CommCenter",
"senderImagePath" : "\/System\/Library\/Frameworks\/CoreTelephony.framework\/Support\/CommCenter"
}]

The first object contains a value for the attribute category, which the next lacks, but there is a common set of around 16 attributes which can occur.

The great majority of examples given in blog articles and books consist of a single object, or a few objects with the same attributes. So I wanted to create JSON data of
[{
first = "the first";
second = "the second";
},
{
first = "not the first";
third = "the third";
}]

In my test model, I want to parse that so I can generate an output of something like
first&second&third
the first&thesecond&
not the first& &the third

where & is an arbitrary delimiter, such as a comma for CSV, or a Tab for tab-delimited text. The first line or row contains the column headers, the complete sequence of attributes.

The first battle that I had with the Playground was getting it to create a String which could be converted to data to pass to JSONSerialization.jsonObject() for conversion into Swift or Foundation data. In the end, I bludgeoned the Playground into accepting:
let data1 = "[{\"first\": \"the first\", \"second\": \"the second\"}, {\"first\": \"not the first\", \"third\": \"the third\"}]"
let theData = data1.data(using: String.Encoding.utf8)

which I then passed to the function, and got an Any or Optional return:
let json = try? JSONSerialization.jsonObject(with: theData!, options: [])

According to Apple, you should then be able to cast that into a Swift array using
let sjson1 = json as? [String: Any]
but that returns nil. Instead, I was able to cast it into an NSArray:
let json1 = json as? NSArray
which returns
(
{
first = "the first";
second = "the second";
},
{
first = "not the first";
third = "the third";
}
)

This array consists of two objects, which can be cast into NSDictionary but not Swift’s Dictionary:
let json2 = json1?.firstObject as? NSDictionary
for example, which returns
{
first = "the first";
second = "the second";
}

Within that, I can access the values using their dictionary keys, for example
let json3 = json2?.object(forKey: "first")
returns the string
the first

What happens, then, when I access that dictionary using an absent key:
let json4 = json2?.object(forKey: "third")
When printed, that appears as nil.

So I can test for nil and return an appropriate string instead:
if (json4 != nil) {
json6 = json4 as! String
} else {
json6 = " "
}

So, once Consolation has converted JSON data returned from log show into Foundation data, I need to iterate through it as an NSArray. Within each object in that array, I need to cast it to NSDictionary, then look up the value for each of the keys, testing for a nil return and substituting a suitable empty string when the key is missing from that object.

My next task is to implement that in Consolation, and try it with some real data. I may be some time…