More fun scripting with Swift and Xcode: Finding apps, and calls that don’t fail

There were two more important features that I wanted to build into HelpHelp: the ability to save a text (preferably CSV) version of the index to Help Books, and ‘registering’ the Help Book in a chosen app.

Because I configured HelpHelp as a document-based app from the outset, enabling it to save a text file was very straightforward. All the action happens in the Document.swift source, where I had to implement a section of code to return the Data to be written. I wondered whether to be smart and try to write some really compact and elegant Swift, but in the end settled for a solution which should be clear and easily maintained:
var theLongStr = ""
for item in theVC.theHelpList {
theLongStr += "\"" + item[1] + "\", \"" + item[0] + "\", \"" + item[2] + "\", \"" + item[3] + "\"\n"
}
let fileContentToWrite = theLongStr + "Contents of ~/Library/Caches/com.apple.helpd/HelpCache.plist at " + theVC.theNowStr
return fileContentToWrite.data(using: String.Encoding.utf8) ?? Data()

helphelp08

This simply iterates through the outer array, writing an inner array on each line, with commas as separators. At the end, it tags on a line of comment to give the index file path, and the time that it had last been consulted.

My more speculative venture in HelpHelp is trying to convince Help Viewer to open a particular Help Book. Normally, this is a task left to the helpd service, which watches the Applications (and Applications/Utilities) folder. When a new or updated app is added, it opens the app bundle and checks to see if there’s a Help Book. If it finds one, it then adds its details to the Help index stored in ~/Library/Caches/com.apple.helpd. Because it handles Help Books through the app’s signature (ID), for each signature it has a maximum of one registered Help Book.

Studying the very limited access provided to the Help system, I noticed registerBooks(in: Bundle), which calls on helpd to add the Help Book for a specified app bundle to its index. This is mainly intended so that an app can register Help Books in plugins and similar components, but seems to be used by Xcode to register the Help Book of newly-built apps too.

My code runs as an Action from the additional button on the HelpHelp window.

@IBAction func buttonRegister(_ sender: Any) {
var theAnswer = false
let FS = NSOpenPanel()
FS.canChooseFiles = true
FS.canChooseDirectories = false
FS.allowsMultipleSelection = false
FS.begin { result in
if result == NSFileHandlingPanelOKButton {
guard let theUrl = FS.url else { return }

The opening displays the standard Open File dialog, called in the same way that I have called the File Save dialog before. Although apps are bundles, and thus really folders, as far as the Open File dialog is concerned, they are files not directories. Armed with the resulting URL of the selected file, the next task was to test whether it is a bundle.

I then entered another of those terrible holes in macOS/Xcode/Swift: how to test for a selected item being a bundle, or an app. I found two routes, one based on URLs which got really messy, the other relying almost entirely on deprecated calls. After a lot of messing around, I decided that the simplest and most reliable way was to check the filename in the URL for the distinctive app extension:
do {
if (theUrl.pathExtension == "app") {

The next thing to do is to get the URL as a bundle. In theory, if that fails, it should return nil; in practice what often occurred was not nil, but not a usable bundle either, and any attempt to test for that resulted in a crash:
if let theBundle = Bundle.init(url: theUrl) {

Next we set the upper text box to give the app name:
self.textBundleName.stringValue = theUrl.lastPathComponent

To call registerBooks(), we must first get the shared NSHelpManager instance, then use that to make the call:
let theHelpMan = NSHelpManager.shared()
theAnswer = theHelpMan.registerBooks(in: theBundle)

The odd thing with this is that it returns a Boolean, which is supposed to be set to false if registration fails. Even when fed folders which are nothing like bundles, and don’t contain Help Books, this returns true, which suggests that its return value may be an unreliable indicator of success. We then handle various errors and problems as we get out of the nested conditionals:
}
} else {
self.textBundleName.stringValue = "app package not supplied,"
}
}
if theAnswer {
self.textRegResult.stringValue = "registered successfully."
} else {
self.textRegResult.stringValue = "failed to register."
}
}
}
}

Properly formatted, this reads:

helphelp09

Not a lot of code, and syntactically quite simple, but it has taken a day to get it to work correctly.

Amazingly, registerBooks() seems to do the job. I am now wondering whether it might also be useful to wrap into a command tool, as there are no other tools which do anything useful with the Help system.