SIGPIPE 13

String List Value Transformer

December 10th, 2004

It's been a while since I last wrote something — the reason for this is that I've been busy working on TextMate, but now that I have a free moment, let me tell you about this general tag value to string transformer I use when I bind radio or popup buttons to my user defaults (to get a string stored in the user defaults instead of an integer).

The NSMatrix and NSPopupButton classes have a few binding properties for the selected item, but the only one which I find useful is selectedTag, since it can stay the same even when changing the GUI (adding, removing, or renaming items).

The “problem” with this property is that it gives an integer. Perhaps a non-issue for some, since we can use enumerations or constants in our code to hide the integer value behind a symbolic name, but when storing the selected item in a property list (as is the case with the user defaults) the symbolic name is lost, making it less human readable.

My solution is to use a value transformer for this binding property, which converts the integer to an NSString and vice versa.

Rather than write a value converter for each and every popup or radio button, I have added a general class to my trusty CocoaExtensions.h which allow me to write:

[OakStringListTransformer
   createStringListTransformerWithName:@"MyTransformer"
   andObjects:@"foo", @"bar", @"fud", nil];

This creates and registers an NSValueTransformer subclass which converts the integers 0, 1, and 2 to the strings foo, bar, and fud and also does the opposite conversion. So in Interface Builder I can write MyTransformer in the value transformer field for my radio or popup buttons, and the integer based tag values are restrained to Interface Builder.

Full code for the NSValueTransformer subclass follows.

@interface OakStringListTransformer : NSValueTransformer
{
   NSArray* stringList;
}
+ (void)createStringListTransformerWithName:(NSString*)aName andObjects:(id)firstObj, ...;
@end
 
@implementation OakStringListTransformer
+ (Class)transformedValueClass         { return [NSNumber class]; }
+ (BOOL)allowsReverseTransformation    { return YES; }
 
+ (void)createStringListTransformerWithName:(NSString*)aName andObjects:(id)firstObj, ...
{
   ASSERT(firstObj != nil);
 
   va_list ap;
   va_start(ap, firstObj);
   NSMutableArray* list = [NSMutableArray array];
   do {
      [list addObject:firstObj];
   } while(firstObj = va_arg(ap, id));
   va_end(ap);
 
   id transformer = [[OakStringListTransformer new] autorelease];
   [transformer setValue:list forKey:@"stringList"];
   [NSValueTransformer setValueTransformer:transformer forName:aName];
}
 
- (id)transformedValue:(id)value
{
   unsigned i = value ? [stringList indexOfObject:value] : NSNotFound;
   return i != NSNotFound ? [NSNumber numberWithUnsignedInt:i] : nil;
}
 
- (id)reverseTransformedValue:(id)value
{
   unsigned i = value ? [value unsignedIntValue] : NSNotFound;
   return i < [stringList count] ? [stringList objectAtIndex:i] : nil;
}
@end

[by Allan Odgaard]


2 Responses to “String List Value Transformer”

  1. Scott Stevenson Says:
    December 11th, 2004 at 05:53

    Have you considered using selectedObject? It seems like it does the same thing you have here.

  2. Allan Odgaard Says:
    December 11th, 2004 at 06:07

    Correct me if I'm wrong, but selectedObject is not available as a binding property unless content and contentValues are also bound.

    If these are bound, it must be to an array (controller) and contentValues is the key path to the label of the item (which must go “through” the array controller). I am however not interested in getting the label of the item stored in my defaults (actually an object having the label as a key (path)) as that would tie me to the current phrasing and rule out localization.


Leave a Reply