Swift’s dictionaries are potentially very useful, but can seem very complex when you move away from the simple flat examples so often given in documentation. Sometimes it is hard to avoid them: if you need to work with property lists, they most naturally translate into dictionaries for you to access them in code. Look at most property lists, though, and they are far from flat, and often nest dictionaries like Russian dolls.
Here are some examples which I have been using recently in MakeLogarchive and now in Woodpile.
When Woodpile parses a logd
log file, it turns its data into a large nested dictionary. Within that is an entry for each file, using the filename as its key. That value is itself a dictionary, which contains three keyed strings containing the date, time, and total size of the log file, and a dictionary of dictionaries for each process. The process dictionaries use the process name as their key, and contain two keyed strings, containing the load figure for that process, and its percentage of the total.
These are kept in a top-level variable, declared thus:
var theFullStatsDict: [String: Any] = [:]
which accommodates any hashable data type, including strings and dictionaries.
This is built from the leaves inwards to the centre. This function (after it has extracted the relevant data as strings) builds a succession of small dictionaries of load and percent against the process name as its key, returning that to its caller
func analyseStats(theArr: [String]) -> Dictionary<String, Any> {
var theTempDict: [String: Any] = [:]
for item in theArr {
if !item.isEmpty {
let theLineArr = item.components(separatedBy: ",")
if (theLineArr.count > 2) {
extract the strings from each line in the array to give theLoad, thePercent, and procName
let theMiniTDict = ["load": theLoad, "percent": thePercent]
theTempDict[procName] = theMiniTDict
} } }
return theTempDict }
The warning here about the forced cast from String?
to String
is interesting: according to that message, I should try using as String
rather than as! String
, but that then generates an error. You’ll see similar warnings about forced casts below, which pose the same puzzle. If you know the solution, I’d be fascinated to hear of it, please.
The caller then adds that dictionary of dictionaries to more strings, which is set against theFilename as its key
func getPersistStats(theStr: String) {
…extract the strings from the log file info to insert in this dictionary, then build the dictionary of procName dictionaries
let theProcsArr = Array(theLines[5...theLineCount])
let theProcsDict = self.analyseStats(theArr: theProcsArr)
and tuck them all within the overall entry, against the key theFilename
let theMiniPDict = ["date": theDateString[0], "time": theDateString[1], "total": theTotal, "procs": theProcsDict] as [String : Any]
theFullStatsDict[theFilename] = theMiniPDict
}
To unpack these from theFullStatsDict, we reverse the process, starting at the trunk and working out to the leaves. But I want to sort the order according to the filename, so I have an array of filenames, which I sort first. This then gives the order of filename keys to use to access theFullStatsDict
func displayFullStats() -> String {
var theOutStr = ""
self.theFileArray.sort()
now iterate through the filenames in the sorted array
for item in self.theFileArray {
if let theOutDict = self.theFullStatsDict[item] {
now theOutDict is the dictionary we want to look inside, so cast it to the right type
let theOut2Dict = theOutDict as! Dictionary<String, Any>
and get the string values for its keys
let theD1 = theOut2Dict["date"] as! String
let theD2 = theOut2Dict["time"] as! String
let theD3 = theOut2Dict["total"] as! String
the last value is a dictionary of dictionaries, which we iterate through to examine all its leaves, the individual processes
let theD4 = theOut2Dict["procs"] as! Dictionary<String, Any>
for (procName, procVals) in theD4 {
once again, we have to cast that into the right type in order to access its contents
let theOut3Dict = procVals as! Dictionary<String, String>
let theD5 = theOut3Dict["load"] as! String
let theD6 = theOut3Dict["percent"] as! String
finally glue all the string together to make a line in CSV format, and return the twenty lines
theOutStr = theOutStr + theD1 + "," + theD2 + "," + theD3 + "," + item + "," + theD5 + "," + theD6 + "," + procName + "\n"
self.theProcsSet.insert(procName)
} } }
return theOutStr }
When the user selects a process name to display, a variation of that is required, which picks only the selected process
func displayPartialStats(theProcName: String) -> String {
var theOutStr = ""
self.theFileArray.sort()
for item in self.theFileArray {
if let theOutDict = self.theFullStatsDict[item] {
let theOut2Dict = theOutDict as! Dictionary<String, Any>
let theD1 = theOut2Dict["date"] as! String
let theD2 = theOut2Dict["time"] as! String
let theD3 = theOut2Dict["total"] as! String
let theD4 = theOut2Dict["procs"] as! Dictionary<String, Any>
now instead of iterating through all the process dictionaries, we pick just the one(s) which match the selected process name
if let theOut3Dict = theD4[theProcName] {
let theOut4Dict = theOut3Dict as! Dictionary<String, String>
let theD5 = theOut4Dict["load"] as! String
let theD6 = theOut4Dict["percent"] as! String
theOutStr = theOutStr + theD1 + "," + theD2 + "," + theD3 + "," + item + "," + theD5 + "," + theD6 + "," + theProcName + "\n"
} } }
return theOutStr }
There is one other function which works with dictionaries, to convert the contents of a property list, add some more dictionaries to it, then save that as another property list. Although much of this is a matter of structuring the dictionaries correctly, from the leaves inwards, it also involves removing one of the objects from the read property list.
func makeInfoFile(destURL: URL) {
var theImportDict = NSMutableDictionary()
let theVerPath = destURL.path + "/Extra/version.plist"
theImportDict = NSMutableDictionary.init(contentsOfFile: theVerPath)!
having imported the property list into a mutable dictionary, the value and key of the Version needs to be removed
if (theImportDict.count > 0) {
theImportDict.removeObject(forKey: "Version")
}
the code then obtains some additional values to be embedded in this dictionary, and assembles each part from the leaves inwards
let theBaseEntry = ["ContinuousTime": currentTime, "UUID": theUUIDStr] as [String : Any]
let theOTR = ["OldestTimeRef": theBaseEntry]
let theTTL = theImportDict as Dictionary
let theSpecialM = ["OldestTimeRef": theBaseEntry, "TTL": theTTL] as [String : Any]
now put them altogether into the top-level dictionary
let theLot = ["EndTimeRef": theBaseEntry, "LiveMetadata": theOTR, "OSArchiveVersion": 3, "OSLoggingSupportProject": "libtrace-518.70.1", "OSLoggingSupportVersion": theSupVer, "PersistMetadata": theOTR, "SpecialMetadata": theSpecialM] as [String : Any]
let theExportDict = NSMutableDictionary()
theExportDict.setDictionary(theLot)
finally, write the new dictionary out as a property list
let theDest = destURL.path + "/Info.plist"
let outURL = URL.init(fileURLWithPath: theDest)
let theResult = theExportDict.write(to: outURL, atomically: false)
if !theResult {
self.appendOutputText(string: "Couldn't save exported file.")
} }
You may well come up with more compact and idiomatic ways of coding this in Swift, but I hope my pedestrian examples above show how it is not difficult to work with nested dictionaries, and how versatile they can be. They also (even in my novice hands) seem to work quite quickly: the code to select and build CSV data for an individual named process runs in no time at all, although parsing the whole logd
log output is significantly slower.