Previous: A critique of mathematical objectivity Articles index Jeff Johnson (My apps, PayPal.Me, Mastodon)
December 16 2025
The other day I released Link Unshortener version 19.8, which migrated its UserDefaults from its app container to a group container, in preparation for future changes. Unfortunately, my migration code had a bug that I missed in testing. It’s a minor bug that doesn’t actually affect users at present, and perhap…
Previous: A critique of mathematical objectivity Articles index Jeff Johnson (My apps, PayPal.Me, Mastodon)
December 16 2025
The other day I released Link Unshortener version 19.8, which migrated its UserDefaults from its app container to a group container, in preparation for future changes. Unfortunately, my migration code had a bug that I missed in testing. It’s a minor bug that doesn’t actually affect users at present, and perhaps it never will affect users, but theoretically the bug could make it more complicated for me if I need to alter the behavior of the app’s UserDefaults.
In this blog post I’ll talk about NSUserDefaults, because I still write Objective-C, believe it or not. Other than language syntax, there’s no relevant difference with the Swift API UserDefaults. The bug involves the method [NSUserDefaults registerDefaults:]. Below is the full discussion from its documentation.
Call this method shortly after launch to specify the default values for your app’s settings. This method assigns the key-value pairs you provide to the registration domain, which is typically the last domain in the search list. The registration domain is volatile, so you must register the set of default values each time your app launches.
To access defaults in the app container (~/Library/Containers/), you use the method [NSUserDefaults standardUserDefaults], whereas to access defaults in a group container (~/Library/Group Containers/), you use the method [NSUserDefaults initWithSuiteName:]. Below is the first paragraph of the discussion from its documentation.
Use this method to create a defaults object that reads settings from the custom domain you specify. For example, you might use this method to access settings you share among multiple apps or between your app and an app extension. The returned object writes settings to the domain you specified. Every instance of ``UserDefaults shares the contents of the argument and registration domains.
Incidentally, I think that the strange use of ``UserDefaults above reveals that the documentation was originally written in Markdown. Enclosing a string in backticks formats the string as code in Markdown. Thus, my theory is that there’s a typo in the documentation: the intention was to use `UserDefaults`, which would display as UserDefaults.
The previous parenthetical paragraph was delaying the climax: “Every instance of UserDefaults shares the contents of the argument and registration domains.” In other words, the result of calling registerDefaults on the object returned by [NSUserDefaults initWithSuiteName:] is the same as calling registerDefaults on the object returned by [NSUserDefaults standardUserDefaults]! Yet the documentation for registerDefaults does not mention this fact.
How did this become a Link Unshortener bug? In the NSApplicationDelegate method applicationWillFinishLaunching, I call [NSUserDefaults initWithSuiteName:] and registerDefaults to register the default values of Link Unshortener settings. Then I check whether the app container settings need to be migrated. If migration is necessary, then I call [NSUserDefaults setObject: forKey:] on the group defaults, using [NSUserDefaults objectForKey:] from the app defaults. If the default key has never been set in the app defaults, then [NSUserDefaults objectForKey:] should return nil. Or so I thought! But at that point registerDefaults has already been called on the group defaults object, and the app defaults object shares the registration domain with the group defaults object, so [NSUserDefaults objectForKey:] returns a non-nil value, which gets saved in the group defaults.
Again, this bug is not an immediate problem for the user. If a setting was never saved in the app defaults, then the value specified by registerDefaults is still used, just like it was in the previous version of Link Unshortener. The only problem is if it ever wanted to change the values specified by registerDefaults, for example, by changing the default value of a boolean setting from true to false. New users of Link Unshortener would get the new default value, but longtime users of Link Unshortener whose settings were migrated would still use the old default value, because the old default value is saved in the group container when the settings are migrated.
I may or may not release a Link Unshortener update to redo the migration correctly. We’ll see. In any case, now you know to be careful about the UserDefaults registration domain.
Jeff Johnson (My apps, PayPal.Me, Mastodon) Articles index Previous: A critique of mathematical objectivity