Wednesday, December 17, 2025

UserDefaults.register(defaults:) Footgun

Jeff Johnson (Mastodon):

“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.

Previously:

7 Comments RSS · Twitter · Mastodon


It's documented:

> 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.

https://developer.apple.com/documentation/foundation/userdefaults/register(defaults:)



Léo, it looks like you didn't read the blog post, which in the second paragraph links to the same documentation as your first comment.

And the issue isn't that the registration domain is volatile; of course we knew that.


No, not the volatility. The "the UserDefaults object searches its domains in a specific order until it finds the value you want" part. The registration domain is always there for every NSUserDefaults object.


> No, not the volatility. The "the UserDefaults object searches its domains in a specific order until it finds the value you want" part.

Again, this was already known.

And again, the blog post never claims that the behavior is undocumented. If you read the blog post, you would know that. To the contrary, the blog post links to and quotes the documentation.


The most misleading aspect is that registerDefaults is an instance method rather than a class method. So you can't call [NSUserDefaults registerDefaults:], you have to call [[NSUserDefaults standardUserDefaults] registerDefaults:] or [[[NSUserDefaults alloc] initWithSuiteName:] registerDefaults:], which suggests that you could register different defaults for the two instances. And of course the two instances are completely separate when it comes to searching for a key: your app defaults don't fall back to your group defaults, nor vice versa.


@Léo It is documented, but the reason I linked to this is that I think the behavior is non-obvious and not well known. It’s intuitive that the argument domain is shared because it’s provided once to the process. But the logical assumption is that since registration happens on the instance, not the class, that it would be isolated to that instance.

Leave a Comment