Building a better console replacement: an exercise in Xcode 1

One common reason for writing an app for macOS, often using a scripting language such as AppleScript, is to provide a friendly front-end to command line tools. My previous example showed how to do this for a trivially simple tool, the say command, which speaks the string which you pass to it. Now it’s time to try Xcode and Swift out for a real-world tool, a better replacement for LogLogger, my substitute for the Console app in Sierra, currently written in AppleScript with some borrowed AppleScriptObjC code for its dialog.

As before, I am using the current release of Xcode (8.1), using Swift 3.0.1, and running on Sierra 10.12.1.

I am going to work incrementally, starting with just a basic dialog to retrieve the last period of Time Machine entries from the logs. It is going to be as much an exploration as an implementation, but at the end of it I want to be able to deliver the Consolation app, signed and ready for use.

I created a new project in Xcode just as before, but this time using a Storyboard, as this is likely to be more visually complex than that simple project. This means that instead of laying the dialog up on the window, we do so on a View Controller, which is shown below it. This added layer of complexity can give considerable flexibility, which I may need as the project progresses.

consoldev01

To begin with, the commands which this dialog constructs will not be sent for execution, but merely displayed in a static text Label. So I first add a Push Button, just as before, which is named Get log, then a Label below it, to contain the text generated from the dialog when the user presses the Get log button.

consoldev02

consoldev03

These two controls then need to be wired into the ViewController.swift source code, which is the only code file which I will be working with for the moment.

consoldev04

The button is wired in as before, as an Action which I have named btnGetLog, and which will generate the string to be displayed in the Label. The Label is therefore an Outlet, named theCmdOut, whose code is placed above the Action code for btnGetLog.

consoldev05

There are only two other controls which I am going to add to this initial build: a Text Field with Number Formatter into which the user will type an integer for the time period of log to be captured, and a Pop Up Button which will contain the units of time (seconds, minutes, hours, days). These are easily added to the View Controller.

The Text Field with Number Formatter has both an Outlet, textTimePeriod, and an Action, txtTimePeriod. This is so that it both sets its value (Action), and that value can be retrieved readily (Outlet). It may be possible to simplify this to an Action alone, but that didn’t seem to work here.

The Pop Up Button needs to be loaded with the titles that I want in the viewDidLoad() function. This first removes all existing items, then adds the list of new items ["sec", "min", "hour", "day"]. It needs an Outlet as popupTimeUnits so that its current setting can be accessed when the Get log button is pressed.

For the time being, I have split the command string up into two parts, into which I will glue digits giving the period, and the single letter for the unit of time, e.g. 4h means four hours. The 4 is drawn from the textTimePeriod outlet, and the h from the popup menu setting at the time. All this is straightforward, but generating the short string from the outlets is not.

You could manually extract the string entered in the textTimePeriod.stringValue, and then convert that into an integer, and back to a string to insert in the command. Much neater is to access the built-in integer conversion provided by reading textTimePeriod.integerValue. That is accessed in the Text Field’s Action textTimePeriod, which sets theTimePeriod to that integer value, then that is converted to a string when assembling the command.

I wasted a great deal of time discovering how to convert one of the three or four letter popup menu items to their first character to form the other part of the command. I know that Swift is a young language, but would you believe that it has no direct way of accessing the first character in a string, as a string? If you think this is just me, browse this discussion on StackOverflow, where a series of Swift experts have tried to solve the problem of returning the nth character as a string, from Swift version 1.2 right up to Swift 3.

In the end, I settled for
theTimeLet = String(theTimeSelStr[theTimeSelStr.startIndex])
which not only snuck past Xcode’s checks, but also seems to work. I cannot believe that a modern programming language has reached version 3 without acquiring such basic string manipulation features, particularly when there are many much more sophisticated things that are easy to accomplish with Swift strings.

consoldev06

This is my finished product, which passes through the build process without error, and seems to run a treat. This app is also properly signed, something which is extremely simple to enable in Xcode once you have a developer ID. Here is the formatted source code:

consoldev07

Or, in commented text:

class ViewController: NSViewController {
// set up some variables and constants
var theTimeSelStr = "sec"
var theTimeLet = "s"
var theTimePeriod = 0
let timeUnitsList = ["sec", "min", "hour", "day"]
// these are the 2 fragments containing the rest of the command which will eventually be executed
let theCommandStr1 = "log show --predicate 'subsystem == \"com.apple.TimeMachine\"' --style syslog --info --last "
let theCommandStr2 = " | cut -c 1-22,43-999 > /Users/hoakley/Documents/0newDownloads/LogLoggerOut00.text"
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view - set the popup menu up
popupTimeUnits.removeAllItems()
popupTimeUnits.addItems(withTitles: timeUnitsList)
theTimeSelStr = (popupTimeUnits.selectedItem?.title)!
}
override var representedObject: Any? {
didSet {
// Update the view, if already loaded.
}
}
// the three outlets
@IBOutlet weak var textTimePeriod: NSTextField!
@IBOutlet weak var popupTimeUnits: NSPopUpButton!
@IBOutlet weak var theCmdOut: NSTextField!
// and two actions, the first for the time period editor
@IBAction func txtTimePeriod(_ sender: Any) {
theTimePeriod = textTimePeriod.integerValue
}
// and the second to run when the Get log button is pressed
// to assemble the string which would be sent for execution,
// but is here displayed in the Label control
@IBAction func btnGetLog(_ sender: Any) {
theTimeSelStr = popupTimeUnits.titleOfSelectedItem!
theTimeLet = String(theTimeSelStr[theTimeSelStr.startIndex])
theCmdOut.stringValue = theCommandStr1 + String(theTimePeriod) + theTimeLet + theCommandStr2
}
}

Xcode is starting to feel less formidable and intimidating, and much more productive. Its autocompletion feature is amazing, and Interface Builder is actually very friendly. Had it not been for the extraordinary gap in Swift over obtaining the first character from a string, this would have been quite quick and straightforward.

I now have the rest of the dialog to add, in incremental fashion, building and testing as I go.