Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Objective-C Custom Getter / Setter

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?

like image 760
Winder Avatar asked Dec 01 '22 04:12

Winder


2 Answers

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.

like image 57
NSGod Avatar answered Dec 05 '22 09:12

NSGod


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.

like image 23
Tim Isganitis Avatar answered Dec 05 '22 10:12

Tim Isganitis