Writing a Custom Value Transformer

The Foundation framework provides several built-in value transformers. You create your own custom value transformers by subclassing NSValueTransformer.

An NSValueTransformer subclass must, at a minimum, implement the transformedValueClass, allowsReverseTransformation and transformedValue: methods. If your custom value transformer supports reverse transformations, you must also implement the reverseTransformedValue: method.

As an example, we’ll create an NSValueTransformer subclass, FahrenheitToCelsiusTransformer, that converts Fahrenheit temperatures to the Celsius scale. This value transformer is also reversible, able to convert Celsius temperatures back to the Fahrenheit scale.

Declaring the Returned Value Class

A value transformer subclass must implement the transformedValueClass class method. This method returns the class of the object that the transformedValue: method returns.

The FahrenheitToCelsiusTransformer class returns an NSNumber, as shown in Listing 1.

Listing 1  Fahrenheit to Celsius transformedValueClass implementation

+ (Class)transformedValueClass
{
    return [NSNumber class];
}

Allowing Reverse Transformations

NSValueTransformer subclasses must also implement the allowsReverseTransformation class method. The subclass implementation should return YES if the value transformer is reversible.

The Fahrenheit to Celsius value transformer is reversible, so the allowsReverseTransformation implementation returns YES, as shown in Listing 2.

Listing 2  Fahrenheit to Celsius allowsReverseTransformation implementation

+ (BOOL)allowsReverseTransformation
{
    return YES;
}

Transforming a Value

The transformedValue: method implements the actual value transformation. It’s passed the object to transform, and returns the result of the transformation. The result must be an instance of the class returned by transformedValueClass.

For maximum flexibility, an implementation of transformedValue: should be prepared to handle a variety of different classes as the value. The Fahrenheit to Celsius transformer can handle values of both NSString and NSNumber classes, by using the floatValue method to convert the value to a scalar.

The result that is returned when the value is nil is dependent on what the value transformer is attempting to do. The Fahrenheit to Celsius implementation of transformedValue:, shown in Listing 3, returns nil in this case.

Listing 3  Fahrenheit to Celsius transformedValue implementation

- (id)transformedValue:(id)value
{
    float fahrenheitInputValue;
    float celsiusOutputValue;
 
    if (value == nil) return nil;
 
    // Attempt to get a reasonable value from the
    // value object.
    if ([value respondsToSelector: @selector(floatValue)]) {
    // handles NSString and NSNumber
        fahrenheitInputValue = [value floatValue];
    } else {
        [NSException raise: NSInternalInconsistencyException
                    format: @"Value (%@) does not respond to -floatValue.",
        [value class]];
    }
 
    // calculate Celsius value
    celsiusOutputValue = (5.0/9.0)*(fahrenheitInputValue - 32.0);
 
    return [NSNumber numberWithFloat: celsiusOutputValue];
}

Reverse Transforming a Value

If an NSValueTransformer subclass supports reverse transformations, it must implement the reverseTransformedValue: method.

Care should be taken when implementing reversible value transformers to ensure that the reversal does not result in a loss of accuracy. In many cases, passing the result of transformedValue: to reverseTransformedValue: should return an object with the same value as the original object.

The Fahrenheit to Celsius implementation of reverseTransformedValue: is shown in Listing 4. The only significant difference between this and the transformedValue: implementation is the temperature conversion formula.

Listing 4  Fahrenheit to Celsius reverseTransformedValue implementation

- (id)reverseTransformedValue:(id)value
{
    float celsiusInputValue;
    float fahrenheitOutputValue;
 
    if (value == nil) return nil;
 
    // Attempt to get a reasonable value from the
    // value object.
    if ([value respondsToSelector: @selector(floatValue)]) {
    // handles NSString and NSNumber
        celsiusInputValue = [value floatValue];
    } else {
        [NSException raise: NSInternalInconsistencyException
                    format: @"Value (%@) does not respond to -floatValue.",
        [value class]];
    }
 
    // calculate Fahrenheit value
    fahrenheitOutputValue = ((9.0/5.0) * celsiusInputValue) + 32.0;
 
    return [NSNumber numberWithDouble: fahrenheitOutputValue];
}