More Scripting in Swift: Styling Attributed Text

Styling text in script apps may seem like an unnecessary complication, but there are plenty of occasions when it can be very useful to the user. In my case, DispatchView needed it to help make sense of its log extracts. Without styling, it’s hard to juggle two scrolling windows full of log entries runnning on different timebases. A little judicious use of emboldening makes a big difference.

dispatchview11

Both the scrolling text views already use NSAttributedText, which can readily be styled by setting attributes for sections of text. All I had to do was get my head around how to set those attributes, then parse the text and style it.

The first change that I needed to make to DispatchView was to bring structure to the text which is to be styled. For example, I want to embolden each line in which the DAS log entries having a message containing the word Rescoring. Searching through the whole of the attributed text to find that, then working out the limits of each line, and emboldening those lines, seems inefficient and prone to error.

Parsing from unstructured data

So I decided to work with the more structured log extracts obtained in JSON format, then convert that to an array of NSDictionary, and parse those. This also makes it much simpler to parse each log entry in more sophisticated ways, including being more selective about which fields to display. For example, the standard output returned from executing the command
log show --predicate subsystem == "com.apple.duetactivityscheduler" --style json --info --last 10m
comes back in text Data. This is then ‘serialized’ into an NSArray of NSDictionary:
let json = try? JSONSerialization.jsonObject(with: data, options: [])
let json1 = json as? NSArray

I then iterate through that array of NSDictionary
for jsonObj in json1! {
let json2 = jsonObj as? NSDictionary
let jsonKey1a = json2?.object(forKey: "timestamp") as! String
var jsonKey1 = ""
var jsonKey3 = ""
var jsonKey4 = ""
if (jsonKey1a.characters.count > 25) {
let startIndex = jsonKey1a.index(jsonKey1a.startIndex, offsetBy: 11)
let endIndex = jsonKey1a.index(jsonKey1a.startIndex, offsetBy: 26)
let timeRange = startIndex..<endIndex
jsonKey1 = jsonKey1a.substring(with: timeRange)
} else {
jsonKey1 = jsonKey1a
}

For the first timestamp field, that removes the initial characters representing the date, and truncates the remaining string to leave just the timestamp.

let jsonKey3a = json2?.object(forKey: processImagePath) as! String
let jsonKey3b = jsonKey3a.components(separatedBy: "/")
jsonKey3 = jsonKey3b.last!

gets the processImagePath field, which is the full path to the process, splits it into the path elements, and takes just the last in those, the name of the process itself. That is repeated for the senderImagePath field giving jsonKey4.

The log message itself I take as it comes:
let jsonKey5 = json2?.object(forKey: theKeys[10]) as! String
and glue together the different fields extracted from that log entry into an attributed string representing the whole of that log entry:
let jsonString1 = jsonKey1 + " " + jsonKey3 + " " + jsonKey4 + " " + jsonKey5 + "\n"
let theTempAttrString = NSMutableAttributedString(string: jsonString1)

This needs to be an NSMutableAttributedString so that it can be styled as a complete line, then added to the end of the whole NSMutableAttributedString.

Styling the line

There are various ways of working with styling in attributed strings. You can use any of several libraries and third-party aids, but my needs here are light, and I just want to do this with a minimum of fuss. One excellent way to get insight and bespoke help on what styling should be applied, and how, is Mark Bridges’ excellent Attributed String Creator, £4.99 from the App Store.

dispatchview24

This allows you to experiment with different styling, generates clean Swift 3 code, and gives you the option of styling primarily using variables or methods. Using this, I was able to make myself a simple example thus:
// Create the attributed string
let myString = NSMutableAttributedString(string:"this is the text\nthis is the text\n")
// Declare the fonts
let myStringFont1 = NSFont(name:"HelveticaNeue", size:12.0)
let myStringFont2 = NSFont(name:"HelveticaNeue-Bold", size:12.0)
// Create the attributes and add them to the string
myString.addAttribute(NSFontAttributeName, value:myStringFont1!, range:NSMakeRange(0,17))
myString.addAttribute(NSFontAttributeName, value:myStringFont2!, range:NSMakeRange(17,16))

I implemented this as follows. Into the View Controller, I declared my font style
let theBold = NSFont(name: "HelveticaNeue-Bold", size: 12.0)

Then after making my single-line attributed string, I test to see whether the message field contains either of the key words
if (jsonKey5.contains("Rescoring") || jsonKey5.contains("DecisionToRun:1")) {

Working out the range to be styled is easy, as it is the whole line
let theRange = NSMakeRange(0, theTempAttrString.length)

The I apply the styling
theTempAttrString.addAttribute(NSFontAttributeName, value: theBold!, range: theRange)
}

and finally, append that styled line of text to the whole, to be displayed in the scrolling text view
theAttrString.append(theTempAttrString)

Putting it all together

Changes to the declarations for the View Controller are shown here:

dispatchview21

The completed parsing and styling function for the DAS view is here:

dispatchview22

And finally the restructured action for the button is here:

dispatchview23

Using Swift 3 as your scripting language therefore opens up full access to Cocoa’s extensive facilities to style text. That’s a great deal, and another compelling reason to use Swift 3 for scripting macOS.