More fun scripting with Swift and Xcode: Reading Plists, building popups, and more

HelpHelp is a classical scripting app, the sort of thing that a few months ago I would have wanted to code in AppleScript. Its first version, published here earlier today, was coded and debugged over a few hours, and as you’ll see, its source is quick and dirty too.

What HelpHelp does is heavily dependent on features which are not readily accessible from AppleScript, but which are straightforward to use from Swift 3. It is thus a good example of the gulf between AppleScript’s current features and those needed of a scripting language for the future. Sorry, AppleScript, but unless someone shows a lot of care and attention, that’s the way it will remain.

The code to operate the dialog’s controls is all fairly standard, and similar to that shown in other scripting projects here. The three novel areas for me are:

  • obtaining the list of installed Help Books,
  • generating a ‘dynamic’ popup menu in association with data,
  • using NSHelpManager to search another Help Book.

From nosing around, I have discovered that the list of installed Help Books is kept in a Property List file at ~/Library/Caches/com.apple.helpd/HelpCache.plist. So the first step was to open that, and gain access to its contents. The snag is that the information that I was after was nested in layers of dictionaries within that file. I worked my way through them using a Swift Playground, where I prototyped the code.

At the top level, that file defines a dictionary containing four values. I needed that associated with the UserBooksKey, one of four keys accessible at that level. Inside that, the data were key-value pairs within a series of dictionaries, whose key is the signature of that Help Book.

helphelp03

The sequence of actions therefore is to remove all existing items in the popup menu, build the full path to the Property List file, and read it into a Dictionary using NSDictionary.init():
override func viewDidLoad() {
super.viewDidLoad()
popupHelp.removeAllItems()
let theFolder = NSHomeDirectory() + "/Library/Caches/com.apple.helpd/HelpCache.plist"
let theDict = NSDictionary.init(contentsOfFile: theFolder)

Checking as I go, I then peel through the layers of the onion to reach the values that I need:
if ((theDict?.count)! > 0) {
let theDict2 = theDict?["UserBooksKey"] as! NSDictionary
if (theDict2.count > 0) {
for (key, value) in theDict2 {
let theDict3 = value as! NSDictionary
let theCount3 = theDict3.count
if (theCount3 > 0) {
let theAccessPath = theDict3["HPDBookAccessPath"] as! String
let theBookID = theDict3["HPDBookIdentifier"] as! String
let theBookPath = theDict3["HPDBookPathKey"] as! String
let theBookTitle = theDict3["HPDBookTitle"] as! String

To load the popup menu, I need an array of menu items (Strings). However I want to put the ‘easy’ names of the Help Books in the menu, and match them against those other values. So I built an array of arrays to do that. The outermost array is simply the series of entries, in my case almost 200 Help Books. The inner array then stores those values for each.
let theBookEntry = [theBookID, theBookTitle, theBookPath, theAccessPath]
theHelpList.append(theBookEntry)
}
}
for item in theHelpList {
theMenuList.append(item[1])
}
popupHelp.addItems(withTitles: theMenuList)
}
}
}

The arrays are declared initially as
var theHelpList = [[String]]()
var theMenuList = [String]()

When I implement sorting, I will need to perform that on theHelpList first, then generate theMenuList to populate the popup.

The two actions, for the popup menu and the Search button, were fairly straightforward.

helphelp04

The popup action gets the index of the selected item, then fetches the corresponding values from theBookEntry and inserts those into the text boxes:
@IBAction func popupHelpSel(_ sender: NSPopUpButton) {
let theInt = popupHelp.indexOfSelectedItem
if (theInt >= 0) {
let theBookEntry = theHelpList[theInt]
textAccessPath.stringValue = theBookEntry[3]
textBookID.stringValue = theBookEntry[0]
textBookPath.stringValue = theBookEntry[2]
}
}

The action for the Search button is a bit more exciting, as it calls the NSHelpManager function find() to display Help Viewer with that search result, for the selected Help Book:
@IBAction func buttonSearch(_ sender: Any) {
if (textBookID.stringValue != "") {
let theSearchString = textSearch.stringValue
if (theSearchString != "") {
let theHelpMan = NSHelpManager.shared()
let theHelpBook = textBookID.stringValue
theHelpMan.find(theSearchString, inBook: theHelpBook)
}
}
}

That’s it. A fun and successful few hours.