Beyond Scripting in Swift: Preferences

Some of the most basic apps, including quick scripting hacks, can benefit from being able to save and restore user preferences. I’m not only referring to a separate Preferences dialog, but basic dialog defaults and the like. This was one of the major shortcomings in my AppleScript log browser LogLogger: every time that you came to use it, you had to set everything up from scratch.

If you have obtained an Apple Developer ID and started to sign your apps, even AppleScript ones, you may have noticed that they start saving their window positions and sizes. This is because signed apps automatically create their own preference file which is used to store such defaults. Putting that preference file to fuller use is not easy in AppleScript: hopefully this article will show how simple it becomes when you’re using Swift 3 instead.

Preference files consist of XML conforming to Apple’s Property List DTD, containing a dictionary of key-value pairs:
<key>myBooleanSetting</key>
<false/>

Reading and writing those using your own code, whether in AppleScript or Swift, is non-trivial. When you’re working with Swift 3 in Xcode 8.2.1, all that is handled for you when you use UserDefaults (formerly NSUserDefaults, which is already available in Foundation in most projects). Unless you want to do very complex things with it, it is straightforward to use.

For each setting that you wish to handle in this way, you need to provide a call to write the setting, and a call to read it, in the appropriate places. In my case, in Consolation, writing preferences is handled as an Action from a button, so the code to save preferences is all placed within that action. Normally, the code to set preferences for a view is run when that view is loaded, so is placed in the view’s viewDidLoad() method.

Each control or data type, which will form an entry in the Property List dictionary, works slightly differently. Here are some common examples, in which I give example code to save that setting, and to retrieve it from preferences and set it in the view. The forKey variables passed are the XML keys which you see in the property list.

Text field

Save:
UserDefaults.standard.setValue(textPredicate1.stringValue, forKey: "textPredicate1")

Set:
if let str = UserDefaults.standard.string(forKey: "textPredicate1") {
textPredicate1.stringValue = str
} else {
textPredicate1.stringValue = "Big"
}

sets the string obtained from the preference file, or a default, here of the string Big.

Popup menu

Save:
UserDefaults.standard.set(popupTimeUnits.indexOfSelectedItem, forKey: "popupTimeUnits")

Set:
let theInt = UserDefaults.standard.integer(forKey: "popupTimeUnits")
popupTimeUnits.selectItem(at: theInt)
theTimeSelStr = (popupTimeUnits.selectedItem?.title)!

The last line sets a variable which tracks the currently selected item in that menu.

Checkbox

Save:
if (checkInfoout.state == NSOnState) {
UserDefaults.standard.setValue(true, forKey: "checkInfoout")
} else {
UserDefaults.standard.setValue(false, forKey: "checkInfoout")
}

Set:
filterTMbutton.state = NSOnState
let b1prefs = UserDefaults.standard.bool(forKey: "checkInfoout")
if b1prefs == true { checkInfoout.state = NSOnState } else { checkInfoout.state = NSOffState }

This first sets a default which remains if there is none set in the preferences file.

Radio buttons

Save:

byondscr24

if (filterTMbutton.state == NSOnState) {
UserDefaults.standard.setValue(true, forKey: "filterTMbutton")
UserDefaults.standard.setValue(false, forKey: "filterPatternbutton")
UserDefaults.standard.setValue(false, forKey: "filterOtherbutton")
} else {
UserDefaults.standard.setValue(false, forKey: "filterTMbutton")
if (filterPatternbutton.state == NSOnState) {
UserDefaults.standard.setValue(true, forKey: "filterPatternbutton")
UserDefaults.standard.setValue(false, forKey: "filterOtherbutton")
} else {
UserDefaults.standard.setValue(false, forKey: "filterPatternbutton")
UserDefaults.standard.setValue(true, forKey: "filterOtherbutton")
}
}

This sets each in the group of three. I am sure that there is a simpler way to do this using the group, but this works fine for now.

Set:
filterTMbutton.state = NSOnState
let b1prefs = UserDefaults.standard.bool(forKey: "filterTMbutton")
if b1prefs == true { filterTMbutton.state = NSOnState } else { filterTMbutton.state = NSOffState }

Again, this sets a default before setting each button in the group.

If you want to be really swanky, you can display and control these using a custom dialog, which then becomes the Preferences item.

I will add these as an extra page to my Swift notes, in view of their usefulness.