Beyond Scripting in Swift: Preferences with and without UserDefaults

The need to make user settings persist between launches of an app is one of my criteria for distinguishing between straight scripting and regular app development. Doing preferences right is, though, an important courtesy which anyone writing apps should attend to.

When you first look at implementing preferences, UserDefaults (NSUserDefaults) is so seductively simple that before you know it, your app behaves properly, saving and loading settings almost effortlessly. As I and others have pointed out, you also drop your users into the mess that cfprefsd creates, whereby their preferences in their Home folder are managed opaquely by an undocumented service.

If you want to implement user preferences without the involvement of cfprefsd, or (in the case of Consolation 3) if you want to allow users to export and import property lists which act as personas or flavours, you have to do a bit more work. How much? Let me show you.

The most important persistent data in Consolation is stored in three lists (arrays), each of which consists of pairs of strings. For this example, I’ll show just one, declared thus
var specPredList = [["none", ""]]
to which the user appends their own string pairs. These are stored in a property list, either as preference settings in ~/Library/Preferences or saved separately, in this form:


Saving preferences

Saving them using UserDefaults requires the following code:
let defaults = UserDefaults.standard
defaults.set(specPredList, forKey: "specPredList")

which then leaves it to cfprefsd to write them out to storage.

Without the luxury of UserDefaults, I work with the path to the property list, for example obtained from a file save dialog if this is an export:
let theSourcePath = url.path
let theExportDict = NSMutableDictionary()
theExportDict.setValue(self.specPredList, forKey: "specPredList")
let theResult = theExportDict.write(toFile: theSourcePath, atomically: false)
if !theResult {
doErrorAlertModal(message: ("Couldn't save exported file."))

To write additional settings in the same code, all you have to do is add them using
theExportDict.setValue(self.theData, forKey: theDataKey)

The additional effort here is tiny.

Loading preferences

Reading preference settings in using UserDefaults is also very simple:
if let theArr1 = defaults.array(forKey: "specPredList") {
specPredList = theArr1 as! [Array<String>]

as each of the functions to fetch a setting returns nil if the key doesn’t exist, or if the value for that key is not an object of the expected type.

Without the assistance of UserDefaults, and working from the path to the property list once again, perhaps obtained from a file open dialog if an export:
var theImportDict = NSMutableDictionary()
let theSourcePath = url.path
theImportDict = NSDictionary.init(contentsOfFile: theSourcePath) as! NSMutableDictionary
if (theImportDict.count > 0) {
self.specPredList = theImportDict.object(forKey: "specPredList") as! [Array<String>]

and that last call is repeated for each additional setting which is stored.

One thing which you might want to do with an imported property list is to merge its contents with existing values. For my pairs of strings, the first is the name, which is used as the menu entry, and can reasonably be used as the criterion for merging. If you want to add only new items to the existing array on the basis of matching names, the following works:
let theSPredList = theImportDict.object(forKey: "specPredList") as! [Array<String>]
for theEntry in theSPredList {
let isThere = self.specPredList.contains { $0[0] == theEntry[0] }
if !isThere {

I’m sure that there are other more elegant ways to do this, but this simple closure remains easy to read and sufficiently efficient.

Having effectively implemented preferences with and without the interference of cfprefsd, I now regret having taken the easy route with UserDefaults for Consolation’s regular preferences, and am tempted to rewrite my code to handle them the more traditional way.


One snag with handling your own preference settings in a property list stored in ~/Library/Preferences is that you must not try to use your app’s default preferences file, which is used to store settings such as the last directory, window size and location. If you do, you’ll maybe get trampled over by cfprefsd in the process. It is much safer to create and access your own separate property list instead.