Enums Keep Popping Up

One of the best ways to learn is to learn from the mistakes of others — it sure beats making your own. Recently, I got that sinking feeling that comes from realizing that you have made a colossal error of judgment, and are going to have to jump through hoops to correct it. What follows is the story so far, in the hope that others may learn from my ignorance.

The tale revolves around the financial modeling software that I develop in my spare time: Trade Strategist (TS). In one particular application window, I have utilized a number of Pop Up Buttons; each button is mapped to an instance variable in a model class, with each item in a button representing a different discrete state of the variable. There is nothing unusual about this at all.

The problems began when I started to incorporate Cocoa Bindings into TS, and decided that because the content values of the Pop Up Buttons were likely to change from time to time, I would like to be able to stipulate the different options entirely in code, rather than Interface Builder (IB). The reasoning was that I would have a better overview of all the different internal states and their labels if everything was defined close together in the program. The alternative would have been to update the Pop Up Buttons in IB by hand each time an option was added or changed. This would not have been the end of the world, but I thought my approach — in which the state labels were effectively treated as data in the model layer — was easier to oversee.

I began by defining static NSString variables for the different labels in each NSPopUpButton, and methods in my controller class to return the values with which to populate each button. I bound one of these methods to the contentValues binding of each pop up. To complete the picture, theselectedValue binding of each button was bound to an NSString instance variable in a model class. Voila!

Unfortunately, I only realized the error in my ways after a version or two of TS had passed. I was representing states internally with the same NSStrings that were used to represent the option in the user interface. Not only is this quite inefficient in memory terms, it is also extremely inflexible. If you want to change the wording of an item in a Pop Up Button, you also have to update unarchiving methods like initWithCoder:, even though the model hasn’t actually changed. It is a classic example of compromising the Model-View-Controller (MVC) doctrine.

To solve the problem — while preserving the ability to define item labels in-code — I introduced a few classes for mapping enum values to NSStrings. The appropriate form for a variable representing a discrete state in the model layer is an enumerated type (i.e. enum); a common way to represent a discrete state in the user interface is a Pop Up Button. These two entities are often used together, and it seems logical to have a simple way of mapping the values of one to the other.

The first class I defined was EnumValueLabel. This simply stores an enum value, and its associatedNSString label:


@interface EnumValueLabel : NSObject {
    int enumValue;
    NSString *label;
}
	
+(id)enumValueLabelWithValue:(int)val andLabel:(NSString *)label;
-(id)initWithEnumValue:(int)val andLabel:(NSString *)label;
	
- (int)enumValue;
- (NSString *)label;
	
@end

The EnumValueLabels are aggregated in the LabeledEnum class:

@interface LabeledEnum : NSObject {
    NSArray *enumValueLabels;
}
	
-(id)initWithEnumValueLabels:(NSArray *)labels;
	
-(NSArray *)enumValueLabels;
	
-(NSArray *)enumValues;
-(NSArray *)labels;
	
-(int)enumAtIndex:(unsigned)index;
-(NSString *)labelAtIndex:(unsigned)index;
	
-(int)enumWithLabel:(NSString *)label;
-(NSString *)labelForEnum:(int)en;
	
@end

The idea is that you initialize a LabeledEnum for each enum that you want to map to aNSPopUpButton. In the following example, a class method initializes and returns a LabeledEnumcorresponding to the enumSignalType.

typedef enum _SignalType {
    BUY_SIGNAL           = 100,
    HOLD_SIGNAL          = 200,
    SELL_SIGNAL          = 300,
    SELL_SHORT_SIGNAL    = 400,
    COVER_SHORT_SIGNAL   = 500
} SignalType;
	
...
	
+(LabeledEnum *)signalTypeLabeledEnum {
    static LabeledEnum *le = nil;
    if ( !le) {
        le = [[LabeledEnum alloc] initWithEnumValueLabels:
            [NSArray arrayWithObjects:
            [EnumValueLabel enumValueLabelWithValue:BUY_SIGNAL
                andLabel:@"Buy"],
            [EnumValueLabel enumValueLabelWithValue:HOLD_SIGNAL
                andLabel:@"Hold"],
            [EnumValueLabel enumValueLabelWithValue:SELL_SIGNAL
                andLabel:@"Sell"],
            [EnumValueLabel enumValueLabelWithValue:SELL_SHORT_SIGNAL
                andLabel:@"Sell Short"],
            [EnumValueLabel enumValueLabelWithValue:COVER_SHORT_SIGNAL
                andLabel:@"Cover Short"],
            nil]];
    }
    return le;
}

These classes are only really useful when they are combined with the third class:EnumValueLabelTransformer. This is a subclass of NSValueTransformer, and is used to bind theselectedValue binding of the NSPopUpButton to the enum instance variable in the model class.


@interface EnumValueLabelTransformer : NSValueTransformer {
    LabeledEnum *labeledEnum;
    NSMutableDictionary *enumValuesForLabels;
    NSMutableDictionary *labelsForEnumValues;
}
	
-(id)initWithLabeledEnum:(LabeledEnum *)labeledEnum;
	
-(LabeledEnum *)labeledEnum;
	
@end

You construct one EnumValueLabelTransformer for each enum, in the application delegate’s initializeclass method:


+(void)initialize {
    [NSValueTransformer setValueTransformer:
        [[[EnumValueLabelTransformer alloc] initWithLabeledEnum:
            [self signalTypeLabeledEnum]] autorelease]
        forName:@"SignalTypeValueTransformer"];
}

The contentValues binding of each Pop Up Button can be bound to a method like the following:


-(NSArray *)signalTypeLabels {
    EnumValueLabelTransformer *trans = (EnumValueLabelTransformer *)
        [NSValueTransformer valueTransformerForName:
            @"SignalTypeValueTransformer"];
    LabeledEnum *en = [trans labeledEnum];
    return [en labels];
}

With all of this apparatus in place, it is possible to define your instance variables as enumerated types, and assign them labels in code. The EnumValueLabelTransformer converts the enum values to the NSStrings that populate the NSPopUpButton. Note also that separation of model and view is now reinstated, because the item labels in the button can be varied independently of the model classes.

I am releasing the three classes described above into the public domain. You can download the source code here. As you might expect, it comes with no warranty whatsoever, and the complete freedom to do with it as you will.

I am also interested in hearing how other people handle the Pop Up Button-Enumerated Value correspondence. Do you just manually enter values in IB? Or have I missed something obvious already in Cocoa? Enter a comment below, and we might all learn something…

Followup:

After writing this piece, it occurred to me that although the solution I have developed is suitable to my circumstances, it is not advisable in general. In particular, because I chose the wrong route initially, the EnumValueLabelTransformer class helps me to easily transform the NSStrings to enumvalues in the initWithCoder: method. However, if you have not made the same mistake, it is not necessary to introduce the classes discussed above, because they basically duplicate functionality already present in the NSPopUpButton class.

So how should you couple an enum to a NSPopUpButton? The most obvious way is simply to manually enter pop up items in IB, and assign a tag to each corresponding to the enum value represented. If, like me, you would prefer to define the labels in source code, you can add items to theNSPopUpButton programmatically, in an awakeFromNib method, for example:


-(void)awakeFromNib {
    [popUpButton removeAllItems];
    [popUpButton addItemWithTitle:@"Buy"];
    [[popUpButton lastItem] setTag:BUY_SIGNAL];
    [popUpButton addItemWithTitle:@"Sell"];
    [[popUpButton lastItem] setTag:SELL_SIGNAL];
    ...
}

In this case, you make no use of the contentValues binding, as was the case in the solution presented above.

Leave a Comment

Filed under Cocoa

Comments are closed.