More fun scripting with Swift and Xcode: alerts and file save

Having had to use AppleScript to cheat my way round running a shell command with privileges, I turn now to another couple of scripting staples: displaying alerts, and file save dialogs.

Alerts

scripting61

These are such fundamental, everyday interface components that they have to be quick and simple to implement. Despite its professed aversion to additional windows, I thought that I’d try these out first in an Xcode Swift playground. The simple lesson: don’t bother. All they will do is hang Xcode completely, so that you have to force quit it. Playgrounds are not yet the robust environment that they need to be, and don’t offer fundamental tools like alerts.

In an app with a window, there are two different types of modal alert which you can readily use: one in a separate window, which dates back to Classic Mac OS, and a sheet which is attached to the app’s window, which is new with Mac OS X / etc.

scripting62

scripting63

The code for this type of basic, one-button alert is really straightforward. Here’s how I implemented the above alert as soon as the app’s window opened, in traditional separate window style:

func applicationDidFinishLaunching(_ aNotification: Notification) {
let alert: NSAlert = NSAlert()
alert.messageText = "Click on the button to check your Mac's security"
alert.informativeText = "You will be asked to enter your admin password."
alert.alertStyle = NSAlertStyle.informational
alert.addButton(withTitle: "OK")
alert.runModal()
}

scripting64

If you want it to appear as a sheet instead, replace alert.runModal() with
alert.beginSheetModal(for: window, completionHandler: nil)

You can do far fancier tricks, with OK and Cancel buttons, testing which the user clicked on, and so on, but none of them gets complex as far as I can see. So whatever else might be arcane, alerts are not – they’re beautifully straightforward.

File save dialogs

I don’t really want to add an alert to LockRattler (there is no point), but one thing that may well be useful is to be able to save the test results to a text file for future reference. But finding a complete up to date code sample showing how to bring up the file save panel and write text out to the selected file was a bit more difficult. In the end I had to resort to some fragments and inspired guesswork to convert from Swift 2 and older versions of macOS and its Swift interfaces.

The other snag with this is that it necessarily makes use of Swift’s error-handling features. Although in Swift 3 these read quite sensibly, my code solution seems quite idiomatic, and differs from some Swift 3 code examples that I have seen. For the moment I’m happy that I can understand its logic, just don’t ask me to alter it.

In LockRattler, until you have clicked the button to obtain the results, there are no results to save. So what I have done is added another button to save the results only when they have been obtained. This button has an outlet:
@IBOutlet weak var theSaveResButton: NSButton!
and an action. It is disabled in the applicationDidFinishLaunching() function by setting
theSaveResButton.isEnabled = false
then enabled in the action function for the button which obtains the data, once those have been obtained, by
theSaveResButton.isEnabled = true

This is the action function for the button to write the results out to a text file:

@IBAction func theSaveResBut(_ sender: Any) {
let fileContentToWrite = theSIPOut.stringValue + "\n" + theXProtOut.stringValue

and so on to build the text to be written out to the file
let FS = NSSavePanel()
FS.canCreateDirectories = true
FS.allowedFileTypes = ["text"]

which should also allow “txt”
FS.begin { result in
if result == NSFileHandlingPanelOKButton {
guard let url = FS.url else { return }
do {
try fileContentToWrite.write(to: url, atomically: false, encoding: String.Encoding.utf8)
} catch {
print (error.localizedDescription)

we should really have an error alert here instead
}
}
}
}

scripting65

Although the guard … do … try … catch structure is not particularly transparent, even in AppleScript these conditionals get a bit messy. What is very neat is fileContentToWrite.write(…), which reminds me of how we used to write complex files out using the old MacApp class library for Classic Mac OS: each class of data had its own methods to read and write themselves.

scripting66

So when you first run the app, the Save results button is disabled.

scripting67

Once you have clicked on the Rattle my Mac’s Locks button and the results have been inserted into the text boxes, Save results is enabled.

scripting68

Clicking on that button brings up a standard file save dialog, as a separate window (there is also a sheet option).

scripting69

And that has all the standard macOS behaviours, like warning you if you are proposing to overwrite an existing file.

So the file save dialog is a bit messy in terms of the syntax required to handle it properly, but otherwise is as straightforward as alerts.

Making scripting interfaces

In this series, I have built code fragments to run shell commands and capture standard output as text, to run shell commands with privileges, to obtain the version number (and potentially other details) of bundles, and to handle file save (and similarly, open) dialogs. Some if not all of these could be usefully packaged to make them easier to access for future scripting work.

With such a multi-paradigm language as Swift, it is hard to know the most appropriate way of doing this. I think that my next task is going to be working that out, and implementing it.