How Preferences do and don’t work

Much of macOS and most apps need to store settings. In Classic Mac OS, those were often saved in resources, which could even preserve each individual document’s window position and size. For Mac OS X, Apple opted for the scheme used in NeXTSTEP, which has evolved into property lists written in XML and saved to Preferences folders. This article explains how these work and can malfunction, in preparation for more detailed accounts to come.

To illustrate this, I’ll take a simple example from one of my own apps: how the user’s chosen font size for text display is handled.

In the app

When an app’s window opens, it has to cope with the situation that there’s no preference file for the app yet, so sets a default value for theFontSize, a floating point number of 12.0. It then asks the UserDefaults subsystem for the value associated with a key of “fontSize”. By this stage, UserDefaults will have looked for that app’s preference file; if it has found it, it will have a dictionary of all the settings within it. This is arranged as pairs of text keys and values for each, in key-value pairs. It should thus have found a pair whose key matches “fontSize”, and can therefore return the value for that key, such as 14.0.

The app’s code then checks whether the returned value is within the accepted range for that setting, changes theFontSize to 14.0, and draws the text within that window at the user’s chosen size.

If the user decides to change the font size, the app sets theFontSize to the new value, such as 16.0, and redraws the text at that size. To ensure that this new size is saved to the preference file, the app will tell UserDefaults to change the value for the key “fontSize” to 16.0. In some cases, rather than change that value now, that could be deferred until the user closes the window or app.

For apps that let UserDefaults handle their settings, the code written by the developer doesn’t open the preference file, read its dictionary of key-value pairs, write any changes made to them, or save and close that file: those are all handled by UserDefaults.

In macOS

When an app is launched, one of the early tasks of macOS is to invite its service cfprefsd, officially named the Defaults Server, to open the app’s preference file, and provide access to the key-value pairs within it. cfprefsd may cache that preference file, and may not write changes to the file until a convenient moment, which could be some seconds later.

Thus what you see in that app’s preference file is just what was last written to disk. In a few seconds, cfprefsd could overwrite many of its key-value pairs with data from memory, or cached to a temporary file somewhere else. Even worse, you might try deleting the preference file, only to discover that cfprefsd writes out a new copy, containing values from a little while ago.

Changing settings

The best and most reliable way to make changes to an app’s settings, or those in macOS, is thus to use the app’s Settings and other means of control. In the case of theFontSize, this is a menu command to make that size larger or smaller. Use that command and the app should negotiate all the complexities and cfprefsd will write the correct value to the correct preference file.

In some cases, you may want to change settings that aren’t exposed within the app. In those of my apps that use an auto-update mechanism, you can disable that by changing a preference that isn’t accessible in the app’s Settings window or menus, with the key “noUpdateCheck”. If that’s set to a value of true, then no check for updates is made when the app first launches.

If the app is already running, or if cfprefsd happens to have its preference file open at the time, changing that key-value pair directly in the app’s preference file may well not work, as cfprefsd might then overwrite your changed version of the preference file. The only reliable way to change the value paired with “noUpdateCheck” in that app’s preference file is to use the command tool defaults, which works through cfprefsd to ensure that the change is properly co-ordinated, thus effective. You then use a command of the form
defaults write co.eclecticlight.AppName noUpdateCheck -bool true
to write the new Boolean value of true to the preference file for the app with the ID co.eclecticlight.AppName, to the key “noUpdateCheck”. That’s guaranteed to be safe and effective, and far more reliable than trying to tamper with the preference file itself.

If you don’t fancy using the defaults command, there are some good GUI utilities that also work through cfprefsd, including Thomas Tempelmann’s free Prefs Editor.

plist03

Which Preferences?

So far I have avoided the question of where all these preference files are stored. Once upon a time, that was fairly straightforward: those for individual user settings went in ~/Library/Preferences, and those for system-wide settings in /Library/Preferences. Sadly, that elegant simplicity is now gone. Look in ~/Library/Preferences/ByHost and you’ll see a load of other property lists with names including UUIDs. If you really enjoy the feeling of panic, look inside some of the folders in ~/Library/Containers and ~/Library/Group Containers, and follow them through to their Library folders, where you’ll see even more. Thankfully all the latter should be symlinked to preference files elsewhere.

These are the result of Preference Domains, which allow a property list in the ByHost folder to override one in the main Preferences folder. At its most long-winded, the defaults command can read something like
defaults -currentHost read -domain com.name.product keyname
where com.name.product is the app ID and keyname is the name of the key in its settings. Instead of specifying the -currentHost, you can omit that, or specify a -host hostname. Try this with a couple of global settings using -globalDomain and you’ll see what a difference it can make.

The important lesson from these ByHost preferences is that sometimes you can get a setting stuck, and no matter what you try you can’t get it to change. If you’re changing the regular property list but there’s an overriding setting in a ByHost preference, then that could easily account for your lack of success.