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:

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


It may not be documented in "registerDefaults", but it is documented in "initWithSuiteName:" where it says, "The argument and registration domains are shared between all instances of NSUserDefaults."

I think this is the appropriate place for it. Most developers only ever use "standardUserDefaults", so why add documentation to "registerDefaults" where it isn't needed? The only time this information would be needed is when the developer is calling "initWithSuiteName:", so that's where it's documented.

I can see how this would be confusing. Developers expect two different objects to have different data. But when those objects are proxies to operating system services, they may share some data, as they do here. But then, this is an ancient API/service. The risks of making architectural changes that would impact long-abandoned, but still used, apps far outweighs the inconvenience of a developer just discovering this quirkiness for the first time in an app still being maintained. And with modern security concerns, "initWithSuiteName:" is even less likely to be called in currently shipping code.


@John They could fix it at the API level by deprecating the instance method and adding a class method. That would make it more clear what’s happening without actually breaking any code. Apple does this kind of stuff all the time, even to fix spelling mistakes.


> And with modern security concerns, "initWithSuiteName:" is even less likely to be called in currently shipping code.

This seems like a bizarre statement. Many sandboxed apps have group containers. As the documentation says, "you might use this method to access settings you share among multiple apps or between your app and an app extension."


@Michael Don't tell me. I'm just a rando on the internet. If it's a problem for you, file a bug report with Apple. Just don't stay up late waiting for a fix. Santa Claus would probably show up first.

@Jeff Well sometimes appearances can be deceiving, eh? I was thinking of the recent requirement to justify use of the User Defaults API in the App Privacy Configuration. But funny you should mention the sandbox. One of my pet peeves is how rarely Apple mentions sandbox implications to APIs. But they do this case, so it seems reasonable that this method is often used to access another app's preferences. Of course there are always some oddball edge cases. Maybe that's why I said "less likely" rather than "never"?


@John You’re right about Apple. I’m telling you because you’re the one making the argument on my blog that there’s a tradeoff between breakage and surprise.

And, yes, the sandbox implications should mentioned for every API.


@Michael I'm not making any such argument at all. I'm pointing out that this behaviour is properly documented. If you choose to publish content on your blog that makes claims that people disagree with, then you're likely to get comments about it.

If you don't want those comments, you can always turn them off. But isn't that engagement? Isn't that why your blog exists in the first place? Would you prefer are more impervious echo chamber perhaps?


@John Then what did you mean by “The risks of making architectural changes that would impact long-abandoned, but still used, apps far outweighs the inconvenience of a developer just discovering this quirkiness for the first time in an app still being maintained”? That seems like an argument, not a comment about documentation. I didn’t even claim that it wasn’t properly documented, nor did the quoted or linked text from Jeff.

It’s fine for people to disagree with me. I learn a lot from the comments. But if someone posts something that I think is wrong, I may push back. If you don’t want me to reply to you, don’t comment.


> That seems like an argument, not a comment about documentation

It was neither. It was just a statement about why developers might be confused about the behaviour of such old APIs and why they still persist into modern times. You are the one who interpreted it as some kind of argument regarding fixing the API. This is Apple we're talking about. Be careful what you wish for.

> I didn’t even claim that it wasn’t properly documented, nor did the quoted or linked text from Jeff.

LOL! It's right there!

> If you don’t want me to reply to you, don’t comment.

It's not that I don't want replies. They are just so tiresome. "Is too!" "Is not!"


> It may not be documented in "registerDefaults", but it is documented in "initWithSuiteName:" where it says, "The argument and registration domains are shared between all instances of NSUserDefaults."

> I think this is the appropriate place for it. Most developers only ever use "standardUserDefaults", so why add documentation to "registerDefaults" where it isn't needed?

I think it should be documented in register defaults. You should document what a method does in the documentation of that method IMO.

Regardless having an instance method behave this way does go against programmer intuition (more reason to justify additional documentation). It would be an improvement to make a class method, mark the instance method deprecated and the just have the instance
method call the class method for backward compatibility. I don’t see a change like that being much different than the deprecation of synchronize which now does nothing but Apple basically has to leave an empty implementation in forever otherwise tons of apps that don’t get updated would crash.

Leave a Comment