Cocoa in the Shell

NSUserDefaults in Preference Pane

In this post I explain how to manage user preferences for a Preference Pane, because it’s slightly different than in a standard Cocoa application.
In a Preference Pane case we can’t use the standard NSUserDefaults, we have to use the CFPreferences API in the Core Foundation framework, so it’s a C-based API, but it’s really easy to use.

First, why can’t we use NSUserDefaults ? The answer is pretty simple, a Preference Pane is like a plug-in, it adds functionalities to an existing app, System Preferences, and so it reuses the preferences file of his parent, in this case com.apple.systempreferences.plist.
that’s why we have to use the CFPreferences API which offers more possibilities.

As this is an example, I will do something very simple with only two preferences, one integer and one string.

The first thing to do is write our defaults values as you will normally do by calling the registerDefaults method of NSUserDefaults. Generally we do this in the initialize class method, so let’s go.

+(void)initialize
{
    const char* const appID = [[[NSBundle bundleForClass:[self class]] bundleIdentifier] cStringUsingEncoding:NSASCIIStringEncoding]; // Get the bundle identifier -> com.yourcompany.whatever
    CFStringRef bundleID = CFStringCreateWithCString(kCFAllocatorDefault, appID, kCFStringEncodingASCII); // Need a CFString
    CFStringRef s = (CFStringRef)CFPreferencesCopyAppValue(CFSTR("stringKey"), bundleID);
    if (!s)
        CFPreferencesSetAppValue(CFSTR("stringKey"), CFSTR("stringValue"), bundleID);
    else
        CFRelease(s);
    CFNumberRef n = (CFNumberRef)CFPreferencesCopyAppValue(CFSTR("intKey"), bundleID);
    if (!n)
    {
        const int val = 123;
        n = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &val); // Need an number object
        CFPreferencesSetAppValue(CFSTR("intKey"), n, bundleID);
    }
    CFRelease(n);
    CFPreferencesAppSynchronize(bundleID);
    CFRelease(bundleID);
}

If you don’t like to use Core Foundation objects, like CFString, CFNumber‚ you can use Foundation objects like NSString, NSNumber and cast them into CF type when needed, it will perfectly work, it is called Toll-Free bridging.

Now that defaults values are written, you will need to update them depending on the action done by the user. So suppose you want to update the integer value when an IBAction is triggered.

+(IBAction)updateIntValue:(id)sender
{
    const char* const appID = [[[NSBundle bundleForClass:[self class]] bundleIdentifier] cStringUsingEncoding:NSASCIIStringEncoding]; // Get the bundle identifier -> com.yourcompany.whatever
    CFStringRef bundleID = CFStringCreateWithCString(kCFAllocatorDefault, appID, kCFStringEncodingASCII);
    const int newsValue = 456;
    CFNumberRef n = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &newsValue);
    CFPreferencesSetAppValue(CFSTR("intKey"), n, bundleID);
    CFRelease(n);
    CFPreferencesAppSynchronize(bundleID);
    CFRelease(bundleID);
}

And to finish this little example, at some point you will probably want to read values from your preferences, so let’s try to get our integer value, to do this there is a convenience method :

+(IBAction)readIntegerValue:(id)sender
{
    const char* const appID = [[[NSBundle bundleForClass:[self class]] bundleIdentifier] cStringUsingEncoding:NSASCIIStringEncoding]; // Get the bundle identifier -> com.yourcompany.whatever
    CFStringRef bundleID = CFStringCreateWithCString(kCFAllocatorDefault, appID, kCFStringEncodingASCII);
    bool b = false;
    const NSInteger i = CFPreferencesGetAppIntegerValue(CFSTR("intKey"), bundleID, &b);
    NSLog(@"%d", i);
    CFRelease(bundleID);
}

You see that using Core Foundation API is a bit different but not really more complicated than the standard NSUserDefaults, the disadvantage with this method is that you can’t use NSUserDefaultsController.