Objective-C Custom Getter / Setter

11,882

Solution 1

Check out "Custom Attribute and To-One Relationship Accessor Methods" in the Core Data Programming Guide. Basically, you should use the primitive getter/setter methods to access/change the value and wrap those calls with KVO notifications.

You will need to add declarations for the primitive accessors:

@interface Portion (PrimitiveAccessors)
- (NSNumber *)primitiveVolume;
- (void)setPrimitiveVolume:(NSNumber *)number; 
@end

Then you need to replace each occurrence of:

[self setValue:number forKey:@"volume"];

with:

[self willChangeValueForKey:@"volume"];
[self setPrimitiveVolume:number];
[self didChangeValueForKey:@"volume"];

And make the corresponding changes in the getter.

Solution 2

Sorry to play devil's advocate, but IMO, it seems like you're trying to address how a value is displayed to the user at too low of a level (in the model object itself; see The Model-View-Controller Design Pattern). Why not use a formatter that works more at the view level to help format the raw NSNumber value to a string that will be presented to the user?

You'd then have a reusable class you could use anywhere you use a number value that represents a volume. The formatter would store a "unitsType" value so it would know how to format the incoming number properly.

I did a quick version by using one of my existing formatters, MDFileSizeFormatter, as a starting point:

#import <Foundation/Foundation.h>

enum {
    MDVolumeFormatterMetricUnitsType            = 1,
    MDVolumeFormatterOurStupidAmericanUnitsType = 2,
    MDVolumeFormatterDefaultUnitsType = MDVolumeFormatterMetricUnitsType
};

typedef NSUInteger MDVolumeFormatterUnitsType;


@interface MDVolumeFormatter : NSFormatter {
    MDVolumeFormatterUnitsType    unitsType;
    NSNumberFormatter            *numberFormatter;
}
- (id)initWithUnitsType:(MDVolumeFormatterUnitsType)aUnitsType;

@property (assign) MDVolumeFormatterUnitsType unitsType;

@end

Then the .m file:

#import "MDVolumeFormatter.h"

#define MILLILITERS_PER_OUNCE 29.5735296

@implementation MDVolumeFormatter

@synthesize unitsType;

- (id)init {
    return [self initWithUnitsType:MDVolumeFormatterDefaultUnitsType];
}

- (id)initWithUnitsType:(MDVolumeFormatterUnitsType)aUnitsType {
    if (self = [super init]) {
        numberFormatter = [[NSNumberFormatter alloc] init];
        [numberFormatter setFormat:@"#,###.#"];
        [self setUnitsType:aUnitsType];
    }
    return self;
}

- (void)dealloc {
    [numberFormatter release];
    [super dealloc];
}

- (NSString *)stringForObjectValue:(id)anObject {
    if ([anObject isKindOfClass:[NSNumber class]]) {
        NSString *string = nil;
        if (unitsType == MDVolumeFormatterMetricUnitsType) {
            string = [[numberFormatter stringForObjectValue:
                       [NSNumber numberWithFloat:
                        [(NSNumber *)anObject floatValue] * MILLILITERS_PER_OUNCE]]
                      stringByAppendingString:@" mL"];

        } else {
            string = [[numberFormatter stringForObjectValue:anObject] stringByAppendingString:@" oz"];
        }
        return string;
    }
    return nil;
}

@end

This could potentially be expanded to do tests on the incoming value and automatically determine the appropriate volume unit. For example, if the floatValue was 16.0, you could use if then logic to return a string of "2.0 cups" instead of 16 oz.

Solution 3

Try using [self setPrimitiveValue:number forKey:@"volume"]; instead.

Solution 4

I would suggest yet another approach. Decide on canonical representation - either ounces or milliliters. This be what volume is actually stored as. Then declare the following getters/setters:

- (void) setOunces:(double)ounces;
- (double) ounces;

- (void) setMilliliters:(double)milliliters;
- (double*) milliliters;

If the canonical volume is milliliters, then:

- (void) setOunces:(double)ounces
{
[self setVolume:[NSNumber numberWithDouble:(ounces* MILLILITERS_PER_OUNCE)]];
}

- (double) ounces
{
return [[self volume] doubleValue]/MILLILITERS_PER_OUNCE;
}

- (void) setMilliliters:(double)milliliters
{
[self setVolume:[NSNumber numberWithDouble:milliliters]];
}

- (double) milliliters
{
return [[self volume] doubleValue];
}
Share:
11,882
Winder
Author by

Winder

Software engineer

Updated on June 15, 2022

Comments

  • Winder
    Winder almost 2 years

    I am using the following NSManagedObject which was automatically generated by Xcode:

    @interface Portion :  NSManagedObject  
    {
    }
    
    @property (nonatomic, retain) NSNumber * volume;
    

    I would like to create a custom getter/setter to convert between ml/oz depending on what the user has set, that way the database always stores the same value and it is automatically converted to the preferred units. My latest attempt looks like this:

    #import "Portion.h"
    #import "SettingHandler.h"
    
    #define MILLILITERS_PER_OUNCE 29.5735296
    
    @implementation Portion 
    
    @dynamic volume;
    
    - (void) setVolume:(NSNumber *) number {
        if ([SettingHandler getUnitsTypeShort] == @"oz") {
            [self setValue:number forKey:@"volume"];
        } else {
            [self setValue:[NSNumber numberWithFloat:[number floatValue] * MILLILITERS_PER_OUNCE] forKey:@"volume"];
        }
    }
    
    - (NSNumber *) volume {
        if ([SettingHandler getUnitsTypeShort] == @"oz") {
            return [self valueForKey:@"volume"];
        } else {
            return [NSNumber numberWithDouble: [[self valueForKey:@"volume"] floatValue] * MILLILITERS_PER_OUNCE];
        }
    }
    

    The setVolume call ends up invoking itself causing an infinite loop. I'm guessing there is a way to do this but I don't know what it is, any ideas?