Storing NSColor in User Defaults

It is often desirable to store the value of an NSColor instance in an application's user defaults. However, NSUserDefaults only supports the storage of objects that can be represented in an property list.

The solution is to use object archiving to write the NSColor instance data to an NSData instance and then store that as the default as shown in Listing 1. This is often done in an application life-cycle exit point such as the applicationShouldTerminate: delegation method.

Listing 1  Storing an NSColor instance in user defaults

// store the value in aColor in user defaults
// as the value for key aKey
NSData *theData=[NSArchiver archivedDataWithRootObject:aColor];
[[NSUserDefaults standardUserDefaults] setObject:theData forKey:aKey];

To read the value back from NSUserDefaults an application retrieves the NSData instance for the required key and unarchives the NSColor instance. The example in Listing 2 demonstrates retrieving the color. This is often done in an application life-cycle entry point such as awakeFromNib.

Listing 2  Retrieving an NSColor instance from user defaults

// read the value of the user default with key aKey
// and return it in aColor
NSColor * aColor =nil;
NSData *theData=[[NSUserDefaults standardUserDefaults] dataForKey:aKey];
if (theData != nil)
    aColor =(NSColor *)[NSUnarchiver unarchiveObjectWithData:theData];

Extending NSUserDefaults to support NSColor

It's possible to take advantage of the support for categories in Objective-C to add NSColor support to the existing NSUserDefaults class, without subclassing.

The example code in Listing 3 and Listing 4 shows an implementation of such a category. The method setColor:forKey: in archives the specified color to an NSData instance and stores it in the user defaults using the specified key. The method colorForKey: retrieves the NSData instance specified by the key, and then unarchives an instance of NSColor using the data.

Listing 3  Contents of NSUserDefaults myColorSupport category .h file

#import <Foundation/Foundation.h>
 
@interface NSUserDefaults(myColorSupport)
- (void)setColor:(NSColor *)aColor forKey:(NSString *)aKey;
- (NSColor *)colorForKey:(NSString *)aKey;
@end

Listing 4  Contents of NSUserDefaults myColorSupport category .m file

#import "NSUserDefaults+myColorSupport.h"
 
@implementation NSUserDefaults(myColorSupport)
 
- (void)setColor:(NSColor *)aColor forKey:(NSString *)aKey
{
    NSData *theData=[NSArchiver archivedDataWithRootObject:aColor];
    [self setObject:theData forKey:aKey];
}
 
- (NSColor *)colorForKey:(NSString *)aKey
{
    NSColor *theColor=nil;
    NSData *theData=[self dataForKey:aKey];
    if (theData != nil)
        theColor=(NSColor *)[NSUnarchiver unarchiveObjectWithData:theData];
    return theColor;
}
 
@end

Establishing Bindings Between Colors and User Defaults

You can easily establish a binding between a user-interface object whose value is a color (that is, an NSColor object) and user defaults. When the user chooses a color preference for something in an application, the binding preserves and restores the preference across successive launches of the application.

To effect the binding, use a ready-made instance of the NSUnarchiveFromDataTransformerName value transformer in Interface Builder. An NSValueTransformer object converts an object value typically in two directions: between the form in which it is displayed and the form in which it is stored. The NSUnarchiveFromDataTransformerName value transformer works by archiving an NSColor object in an NSData object and then, on the other side of the binding, unarchiving the color object from the data object. For this value transformation to work, the archived object must implement the NSCoding protocol using sequential archiving—which NSColor does.

An NSColorWell instance is a user-interface object whose value is a NSColor object. You can drag the color-well object from the Controls palette of Interface Builder onto a view. To establish the binding between this object and user defaults, complete the following steps:

  1. With the color well still selected, open the Bindings pane of the Inspector and expose the value binding.

  2. From the “Bind to” pop-up menu choose Shared User Defaults.

    This action adds an instance of NSUserDefaultsController (“Shared Defaults”) to the nib file window.

  3. Keep the Controller Key field as values but in the Model Key Path field specify a name under which to save the color object (theColor, in this example).

  4. From the Value Transformer combo box select (or enter) NSUnarchiveFromData.

When you’re finished, your setup in Interface Builder should look similar to that in Figure 1.

Figure 1  Establishing a binding between an NSColor value and user defaults
Establishing a binding between an NSColor value and user defaults

If at this point you save your nib file and build your project, you can launch the application, change the color in the color well, quit the application, and then relaunch. The color in the color well is what it was when you last changed it.

Although the foregoing procedure establishes a binding between an NSColor value of a view and user defaults, it does not propagate changes in that value to other objects in the application. You can do that by explicitly setting the color to the restored default when the application launches and, thereafter, by having the first responder handle the changeColor: message whenever the user changes the color. But you can also use bindings so that any change in color value is propagated both to user defaults and applied to a custom view in the application. This requires you to complete the following steps:

Listing 5  Establishing a binding between an NSColor property and NSUserDefaultsController

@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    [theColorBox bind:@"backgroundColor" toObject:[NSUserDefaultsController sharedUserDefaultsController]
        withKeyPath:@"values.theColor"
        options:[NSDictionary dictionaryWithObject:NSUnarchiveFromDataTransformerName forKey:NSValueTransformerNameBindingOption]];
}
@end