From AppleScriptObjectiveC to Swift

Oh this will be fun…

Recently, I decided to try to update my Nagios Manager app from AppleScriptObjectiveC to Swift. There’s a few reasons for this, one, it takes a lot less time to type “Swift” than “AppleScriptObjectiveC”. I mean, I can use the more common “ASOC”, but that doesn’t google as well. Secondly, there’s a few things I’d like to do that are a real pain in the ass to do with ASOC, and some that just won’t work.

I don’t want anyone to take “John is dumping ASOC” from this. I’m not. I like ASOC, and it’s a good higher-level language. But there are things it can’t do well, and Swift does those things well.

Note: I will not be using SwiftUI. It would take a month to get Nagios Manager’s UI working in SwiftUI, and in any event, if this were to ever move to iPads/iPhones, I’d have to redesign the UI completely for them anyway. For now, this is a macOS-only app, and as I’ve talked about before, SwiftUI is a really awful choice for macOS apps that aren’t also going to be on iOS/iPadOS

UserDefaults

Nagios Manager stores its settings in User Defaults. It’s a plist that’s an array of dicts with a boolean I use to see if there’s anything there other than the boolean. I’m kind of lazy and if I use UserDefaults, then I don’t have to deal with paths or locations. It may not be the best way to do this, but it lets me write less code. I like writing less code. The initial ASOC version is:

property theDefaults : missing value --referencing outlet for our NSDefaults object
property theSMSettingsList : {} --settings list array
property theSMDefaultsExist : "" --are there currently settings?

set my theDefaults to current application's NSUserDefaults's standardUserDefaults() --make theDefaults the container for defaults operations

my theDefaults's registerDefaults:{serverSettingsList:{}} --sets up "serverSettingsList" as a valid defaults key  changed to more correctly init as array instead of string. It also deals with nils much better

set my theSMSettingsList to (my theDefaults's arrayForKey:"serverSettingsList")'s mutableCopy() --this removes a bit of code by folding the NSMutableArray initialization and keeps it mutable even after copying the contents of serverSettingsList into it.

 set my theSMDefaultsExist to theDefaults's boolForKey:"hasDefaults" --get the boolean value for the hasDefaults key

 if not my theSMDefaultsExist then --if there are no defaults, let the user know this so they can fix that issue.

try --to catch the error -1 

display dialog "there are no default settings existing at launch" --my version of a first run warning. Slick, ain't it.28 when a user hits cancel

on error errorMessage number errorNumber 

if errorNumber is -128 then
--this error is generated when the user hits "cancel" in the display dialog. This on error sinkholes it that so it doesn't cause any actual problems, since the end result for either Cancel or OK is what we want.

 end if
 end try 

set my theSMStatusFieldText to "If you're seeing this, then there's no servers saved in the app's settings. This tab is where you add them.\r\rYou'll need three things - the server's name, URL and API Key. For the URL, only the first part, i.e. https://server.com/ is needed. The \"full\" URL is generated from that.\r\rThe app itself is pretty simple. You can add or remove servers. Those are saved locally on your mac.\rThose servers are used to pull down user info in the User Manager tab. More info is in the application help in the Help Menu."

else if my theSMDefaultsExist then --there's no point in running loadServerTable: if there's no data to load

 my loadServerTable:(missing value) -- initial load of existing data into the server table.

end if

Yeah, there’s a lot going on there, especially if you aren’t used to ASOC or AppleScript. Also, I really like to comment the crap out of my code.

The first three lines are setting up properties, which are…an odd form of global variable that’s not really a global. I probably abuse them, but for ASOC, properties are your very good friend. They’re initially set up as empty strings, one empty list/record which corresponds to an array/dictionary in Swift and one missing value, which probably comes closes to an NSObject. We mostly use them in ASOC for UI binding, but we use it here for creating our initial NSUserDefaults object.

One of the weird things about going from ASOC to Swift is that ASOC is rather tightly bound around ObjC (you can use ObjC in an ASOC app), but not Swift. So there’s some translational work you have to do there, like Array instead of NSArray, etc.

The next statement creates our UserDefaults object:

set my theDefaults to current application's NSUserDefaults's standardUserDefaults() --make theDefaults the container for defaults operations

There’s some ASOC-isms there that need to be dealt with. The “my” keyword is a way of dealing with scope. If I didn’t have the “my”, I’d have to have theDefaults defined in the function it lives in, on applicationWillFinishLaunching:aNotification specifically. (“on” is another ASOC keyword, ala “func” in Swift.) The “my” allows me to use the property version of “theDefaults”. Oh, and “--” is how you do single line comments in ASOC and AppleScript.

current application's” is another ASOCism, which relates to how generic AppleScript does things. AppleScript is really designed to be an application scripting language, where you use different applications as engines to do work. AppleScript in and of itself isn’t really designed to solve all your problems for you. So the “current application” thing is needed so the ASOC runtime knows what you mean. This statment is saying: “Set the value for theDefaults” to be an instance of NSUserDefaults.” This will be how we are able to use NSUserDefaults methods. This is why we defined theDefaults as missing value in the properties statement.

Next is:

my theDefaults's registerDefaults:{serverSettingsList:{}} --sets up "serverSettingsList" as a valid defaults key changed to more correctly init as array instead of string. It also deals with nils much better

This is one of those cases where a higher level language is much nicer to work with. I don’t have to declare much about serverSettingsList other than it’s some form of array-ish object. This will let it work as either a record (dictionary) or list (array). It’s kind of nice. I could have used a string or what have you here, but it turns out I need an array-ish object anyway, and it handles nils better than strings or other variable types.

Next is:

set my theSMSettingsList to (my theDefaults's arrayForKey:"serverSettingsList")'s mutableCopy() --this removes a bit of code by folding the NSMutableArray initialization and keeps it mutable even after copying the contents of serverSettingsList into it.

So within theDefaults, serverSettingsList, as it turns out, is an array of dicts, or in ASOC terms, a list of records. There’s a few dicts in there, one per nagios server and some for the AD Auth Records Nagios Manager needs to use. So what we do here is say “Shove all the data in serverSettingsList into theSMSettingsList array-ish thing, (which then makes theSMSettingsList a list of records), but also make it an NSMutableArray, because we’ll potentially need to modify it at some point.

Next we want to see if there are any defaults currently on-disk:

set my theSMDefaultsExist to theDefaults's boolForKey:"hasDefaults" --get the boolean value for the hasDefaults key

Now, I could have checked to see if the results of the mutableCopy() were nil, but, that’s only going to be the case if there’s nothing. Within Nagios Manager, there is the ability to delete one, or more, or all servers and settings for those servers. However, that doesn’t delete the defaults plist file. So we could have an empty array, which wouldn’t necessarily return nil, even though there’s no useful data in it. By checking the value of the “hasDefaults” boolean, I get a better answer, one of three:

  1. There’s a defaults file, but no servers listed (false)
  2. There’s no defaults file, (false/nil or the ASOC version of nil, missing value.)
  3. There are defaults in the file, (true)

That reduces down to one of two return values, true/false, which make checking easy. The next part is an if-then which handles this:

if not my theSMDefaultsExist then --if there are no defaults, let the user know this so they can fix that issue.

In Swift, this would be “if !theSMDefaultsExist”. If this happens on launch, there’s no defaults available to load, so let’s tell the user that. We’ll do that via a display dialog statement that will let the user know there’s no defaults to read. We’re going to wrap that in a try block because when you hit the “Cancel” button in a display dialog’s dialog, that generates an error (-128) that we want to trap and sinkhole, since we don’t really do anything based on the response. This is jsut for notification purposes to the hu-mon:

try --to catch the error -1

display dialog "there are no default settings existing at launch" --my version of a first run warning. Slick, ain't it. Generates a -128 when a user hits cancel

on error errorMessage number errorNumber

if errorNumber is -128 then
--this error is generated when the user hits "cancel" in the display dialog. This on error sinkholes it that so it doesn't cause any actual problems, since the end result for either Cancel or OK is what we want.

end if
end try


That’s a basic try-on error block. I could have just left the on error part blank, but just in case there’s ever a different error I want to handle differently, I’ve left a structure in place to do that. As I said, this is a big sinkhole for that error.

This next text block:

set my theSMStatusFieldText to "If you're seeing this, then there's no servers saved in the app's settings. This tab is where you add them.\r\rYou'll need three things - the server's name, URL and API Key. For the URL, only the first part, i.e. https://server.com/ is needed. The \"full\" URL is generated from that.\r\rThe app itself is pretty simple. You can add or remove servers. Those are saved locally on your mac.\rThose servers are used to pull down user info in the User Manager tab. More info is in the application help in the Help Menu."

Is used in the UI for Nagios Manager to help explain more of what’s going on. When you see the Swift code, you won’t see any of this or the swift version of the Display Dialog thing, since I haven’t actually started to build the UI yet.

else if my theSMDefaultsExist then --there's no point in running loadServerTable: if there's no data to load

my loadServerTable:(missing value) -- initial load of existing data into the server table.

end if


If the defaults exist, then we call the loadServerTable function, which loads the array controller that manages the list of servers in the UI.

Since we aren’t passing it any values, it gets a (missing value). One of the reasons for using so many properties is there are things that don’t exist in ASOC, like structs, so where you’d normally pass stuff in other languages, I find it much, MUCH easier to use properties instead.

So now, here’s my first pass at the Swift version:

//create the defautls object
var theNMDefaults = UserDefaults.standard
//the var we'll use to actually hold defaults in the app
var serverSettingsList = [String: String]()
 //register serverSettingsList as a valid defaults key
 theNMDefaults.register(defaults: serverSettingsList)
 //pull the defaults, which ends up being an array of dicts
 var theNMSettingsList = theNMDefaults.array(forKey: "serverSettingsList")
 //check to see if defaults exist at all. Even though 
 //theSMSettingsList will be nil if there's NOTHING as in no 
 //plist at all, there's also the case of the file existing, 
 //but we erased the settings or what have you. So we check 
 //the bool key we use for when the file exists but there's 
 //no settings in it. We're also doing some LIGHT variable 
 //renaming. 

//this will be false if there's nothing there or set to 
//false
 var theNMDefaultsExist = theNMDefaults.bool(forKey: "hasDefaults")

//initial if block
 if !theNMDefaultsExist {
 //display alert stating there are no defaults to read
 } else {
 //do initial loading of defaults into the appropriate 
 //controller. That will happen after we start building out 
 //the UI.
 }

So this block isn’t as complete as the ASOC version, but it’s doing the same basic stuff. Creating a user defaults object, registering with the object, checking to see if there’s any defaults, and setting up the actions if there’s anything there.

So that’s the first bit. This will be a long project, and I’m not working on it every day. (For one, it’s about impossible to test from home.)

I also plan on making all kinds of mistakes, because I’m not a full-time coder, I’m certainly not an expert in swift, and really, this app has a design target of me. So there’s things I’m doing here that someone with more of a clue wouldn’t, but meh. This app works by slinging REST commands back and forth and parsing JSON. Algorithm speed is not that essential here. I can also see that I’m going to miss a lot of the things ASOC did for me as a higher level language.

The next step will be the initial UI setup, a window with 4 tabs, and to get the initial setup working for the server manager part. So fun times.