The next release of Balthisar Tidy will include two awesome features: Services support and (for Yosemite and higher) Action Extension support. Given that Extensions require the containing application to be sandboxed, it finally came time to figure out how to use Apple's “Application Groups” in order to share user defaults.
Although there are many articles out there that describe how this might be done (and they were
indeed a great help), they all unsatisfactorily didn't address the user interface aspect,
specifically how to deal with NSUserDefaultsController
that’s used in Interface Builder so
commonly.
tl;dr:
In your main application create an instance of NSUserDefaults
using the shared suite and then
simply mirror your standardUserDefaults
into it using the notification system.
Problem Description
Sandboxing intentionally makes it difficult for applications to access resources outside of their own sandbox, and this includes the sharing of user defaults.
Balthisar Tidy implements the Tidy Service as a faceless app in the main bundle rather than forcing the entire application to open each time the Service is invoked, and its Action is, of course, packaged as a plugin. Neither the Service nor the Action is entirely useful without being able to access Balthisar Tidy’s user defaults, which control how tidy will process and format the HTML.
Application Groups to the Rescue
Fortunately Apple has allowed the user of Application Groups as a mechanism for sharing user
defaults between applications that are signed by the same development team, and (importantly!)
prefixed with your Developer Team ID, for example, 9PN2JXXG7Y.com.balthisar.Balthisar-Tidy.prefs
.
Both plugins then have access to the shared user defaults by accessing them from the defaults instance created like this:
NSUserDefaults *localDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"9PN2JXXG7Y.com.balthisar.Balthisar-Tidy.prefs"];
The Hangup
Balthisar Tidy (like many applications and frameworks) has come to rely on the always-available
[NSUserDefaults standardUserDefaults]
instance, and a lot of code — instead of abstracting
this away as we should perhaps have done — sprinkles this in liberally. However just because
an application is the main application doesn’t excuse it from the need to write its preferences
into the shared suite if you want to expose those settings to the other applications in your
suite.
Refactoring all of these direct calls into, say, a shared instance in an app is trivial, and it
would allow your application to write into the Application Suite defaults instead of
standardUserDefaults
, but it ignores two critical things:
- Frameworks and Libraries
- Just as you are guilty of unfettered direct use of
[NSUserDefaults standardUserDefaults]
, the folks who wrote some of the libraries you use did this too. I tend not to want to modify third party libraries without an accepted pull request. - Apple’s own code
- Apple’s own code, such as
NSUserDefaultsController
also hard-wires to[NSUserDefaults standardUserDefaults]
without any convenient mechanism to override this. Subclassing could have been an option, but it seems unnecessary and complicates the NIBs for no good reason.
The Solution
Refactor this for your own circumstances, but it’s as easy as:
NSUserDefaults *mirroredDefaults = [[NSUserDefaults alloc] initWithSuiteName:@"9PN2JXXG7Y.com.balthisar.Balthisar-Tidy.prefs"];
…
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleUserDefaultsChanged:)
name:NSUserDefaultsDidChangeNotification
object:[NSUserDefaults standardUserDefaults]];
…
- (void)handleUserDefaultsChanged:(NSNotification*)note
{
NSDictionary *localDict = [[[NSUserDefaults standardUserDefaults] dictionaryRepresentation] objectForKey:JSDKeyTidyTidyOptionsKey];
[mirroredDefaults setObject:localDict forKey:JSDKeyTidyTidyOptionsKey];
[mirroredDefaults synchronize];
}
This example only copies a nested group of defaults in the JSDKeyTidyTidyOptionsKey
(because
Balthisar Tidy doesn’t have a need to share all defaults), but it’s trivial to rework this
to include any or all defaults that you want to copy.
This example is also one-way only because the plugins don’t modify user defaults. If your own
application requires two way exchange, apt use of synchronize
and some simple logic should
make it elementary to do so.
A Word of Warning
Since Mac OS X 10.9 (Mavericks) user defaults are cached by the operating system, even when
synchronize
is used. The user defaults system works perfectly fine with this caching; only
direct access to the preferences’ plist files is risky.
This means that we have to change our lifelong habit of modifying plist files directly to change
the settings in our favorite apps and learn to use the defaults
command in the terminal
instead. In fact many times it’s not sufficient to to delete an application’s preferences file
in order to get a “clean start”; the caching system may very well replace that file with the
cached information at next launch! Using defaults.delete
in terminal will always work. Now
is the time to get used to it.
This is also important if you use any libraries or frameworks that depend on direct file access to share user defaults! There is no guarantee that the file representation is correct. During my research I encountered at least one fairly high profile library that promises to synchronize shared preferences in a sandbox environment. It counts on file access.
Do yourself a favor and stick to using Cocoa’s built-in methods instead, unless you have to
support Mac OS X 10.8 (which doesn’t support initWithSuiteName
).