1

I have observers added to several NSTextFields to monitor changes in each text field. The key of each text field is configured in Interface Builder at Bindings -> Value -> Model Key Path. When the number in one text field is changed, the other text fields automatically update their value. Since an observer was added to each text field, I must remove the other observers to avoid a loop that will crash the app. After the observers are removed, I must add them back to the other text fields so they can be monitored for input by the user. My approach is working fine, but I can see how this can be cumbersome if a lot of observers are added.

Is there a way to streamline this to where I don't have to add and remove observers depending on the user's input?

#import "Converter.h"

@interface Converter ()

@property double kilometer, mile, foot;

@end

@implementation Converter

- (void)awakeFromNib {
    [self addObserver:self forKeyPath:@"kilometer" options:0 context:nil];
    [self addObserver:self forKeyPath:@"mile" options:0 context:nil];
    [self addObserver:self forKeyPath:@"foot" options:0 context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {

    if ([keyPath isEqualToString:@"kilometer"]) {
        [self removeObserver:self forKeyPath:@"mile"];
        [self removeObserver:self forKeyPath:@"foot"];

        NSLog(@"kilometers");

        [self setMile: [self kilometer] * 0.62137119 ];
        [self setFoot: [self kilometer] * 3280.8399 ];

        [self addObserver:self forKeyPath:@"mile" options:0 context:nil];
        [self addObserver:self forKeyPath:@"foot" options:0 context:nil];
    }

    if ([keyPath isEqualToString:@"mile"]) {
        [self removeObserver:self forKeyPath:@"kilometer"];
        [self removeObserver:self forKeyPath:@"foot"];

        NSLog(@"miles");

        [self setKilometer: [self mile] * 1.609344 ];
        [self setFoot: [self mile] * 5280 ];

        [self addObserver:self forKeyPath:@"kilometer" options:0 context:nil];
        [self addObserver:self forKeyPath:@"foot" options:0 context:nil];
    }

    if ([keyPath isEqualToString:@"foot"]) {
        [self removeObserver:self forKeyPath:@"kilometer"];
        [self removeObserver:self forKeyPath:@"mile"];

        NSLog(@"feet");

        [self setKilometer: [self foot] * 0.0003048 ];
        [self setMile: [self foot] * 0.00018939394 ];

        [self addObserver:self forKeyPath:@"kilometer" options:0 context:nil];
        [self addObserver:self forKeyPath:@"mile" options:0 context:nil];
    }
}

@end

Here's a screen shot of the user interface: unit converter screen shot

To help clarify what the code is doing (or suppose to be doing):

User wants to convert feet to kilometer and miles, so he inputs a value into the feet text field. The appropriate conversion factors are used.

The user wants to convert kilometers to miles and feet, so he inputs a value into the kilometer field. A different set of conversion factors are used.

etc...

wigging
  • 8,492
  • 12
  • 75
  • 117

2 Answers2

2

By customizing your setter method and implementing + (BOOL)automaticallyNotifiesObserversForKey:, you can manually update notifications for these properties in a nested way.

The following codes are tested to be working. (Note that I did not use your coefficients and property names).

#define BEGIN_UPDATE [self willChangeValueForKey:@"m"];\
    [self willChangeValueForKey:@"km"];\
    [self willChangeValueForKey:@"f"];

#define END_UPDATE [self didChangeValueForKey:@"f"];\
    [self didChangeValueForKey:@"km"];\
    [self didChangeValueForKey:@"m"];

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)key {
    if ([key isEqualToString:@"f"]||[key isEqualToString:@"km"]||[key isEqualToString:@"m"]) {
        return NO;
    }
    return [super automaticallyNotifiesObserversForKey:key];
}

- (void)setF:(float)f {
    BEGIN_UPDATE
    _m = 0.5*f;
    _km = 0.1*f;
    _f = f;
    END_UPDATE
}

- (void)setKm:(float)km {
    BEGIN_UPDATE
    _km = km;
    _f = 10*km;
    _m = 5*km;
    END_UPDATE
}

- (void)setM:(float)m {
    BEGIN_UPDATE
    _m = m;
    _km = 0.2*m;
    _f = 2*m;
    END_UPDATE
}
ZhangChn
  • 3,154
  • 21
  • 41
  • I didn't use setter methods because the conversion factor must change depending on which unit the user inputs. If the user inputs 1 kilometer then mile = kilometer * 0.62137119 but if the user inputs 1 foot then mile = foot * 0.00018939394. – wigging Jan 23 '13 at 18:09
  • The setter method does not accomplish what I need to do. – wigging Jan 23 '13 at 18:30
2

Assuming that, as I suggested before, you have set your various NSTextFields to be bound continuously to the various properties (kilometer, mile, and foot), one approach is to use a single backing property, let's say foot, and have all other properties affect this one:

NSString * const KeyFeet = @"feet";
NSString * const KeyKilometers = @"kilometers";
NSString * const KeyMiles = @"miles";

+ (NSSet *)keyPathsForValuesAffectingKilometers
{
    return [NSSet setWithObject:KeyFeet];
}

static const double kilometerToFeet = 3280.84;
- (void)setKilometers:(double)kilometers
{
    self.feet = kilometerToFeet * kilometers;
}

static const double feetToKilometers = 0.0003048;
- (double)kilometers
{
    return feetToKilometers * self.feet;
}

+ (NSSet *)keyPathsForValuesAffectingMiles
{
    return [NSSet setWithObject:KeyFeet];
}

static const double milesToFeet = 5280;
- (void)setMiles:(double)miles
{
    self.feet = milesToFeet * miles;
}

static const double feetToMiles = 0.000189394;
- (double)miles
{
    return feetToMiles * self.feet;
}

You'll also need to implement -setNilValueForKey: in case the user clears out the textfields:

- (void)setNilValueForKey:(NSString *)key
{
    if ([key isEqualToString:KeyFeet]) {
        self.feet = 0.0;
    } else if ([key isEqualToString:KeyKilometers]) {
        self.kilometers = 0.0;
    } else if ([key isEqualToString:KeyMiles]) {
        self.miles = 0.0;
    } else {
        [super setNilValueForKey:key];
    }
}

Note: There's no reason to use KVO on self in this context.


For greater precision, multiple backing properties can be used:

@property (nonatomic) double primitiveKilometers;
@property (nonatomic) double primitiveMiles;
@property (nonatomic) double primitiveFeet;

Whenever any of the bound properties (kilometers, miles, or feet) are set, all of these will be set. Each bound property will have its primitive version as the key path which affects its value. The bound property accessors themselves will simply return their primitive counterparts.

NSString * const KeyPrimitiveFeet = @"primitiveFeet";
NSString * const KeyPrimitiveKilometers = @"primitiveKilometers";
NSString * const KeyPrimitiveMiles = @"primitiveMiles";

+ (NSSet *)keyPathsForValuesAffectingFeet
{
    return [NSSet setWithObject:KeyPrimitiveFeet];
}
static const double feetToMiles = 0.000189394;
static const double feetToKilometers = 0.0003048;
- (void)setFeet:(double)feet
{
    _primitiveFeet = feet;
    self.primitiveKilometers = feetToKilometers * feet;
    self.primitiveMiles = feetToMiles * feet;
}
- (double)feet
{
    return self.primitiveFeet;
}

+ (NSSet *)keyPathsForValuesAffectingKilometers
{
    return [NSSet setWithObject:KeyPrimitiveKilometers];
}
static const double kilometersToFeet = 3280.84;
static const double kilometersToMiles = 0.621371;
- (void)setKilometers:(double)kilometers
{
    _primitiveKilometers = kilometers;
    self.primitiveFeet = kilometersToFeet * kilometers;
    self.primitiveMiles = kilometersToMiles * kilometers;
}

- (double)kilometers
{
    return self.primitiveKilometers;
}

+ (NSSet *)keyPathsForValuesAffectingMiles
{
    return [NSSet setWithObject:KeyPrimitiveMiles];
}
static const double milesToFeet = 5280;
static const double milesToKilometers = 1.60934;
- (void)setMiles:(double)miles
{
    _primitiveMiles = miles;
    self.primitiveFeet = milesToFeet * miles;
    self.primitiveKilometers = milesToKilometers * miles;
}
- (double)miles
{
    return self.primitiveMiles;
}
Community
  • 1
  • 1
Nate Chandler
  • 4,533
  • 1
  • 23
  • 32
  • Yes I understand your approach, but the problem is that the conversion factor must change depending on which text field is active. Here's a good javascript example of what I'm trying to accomplish http://bucarotechelp.com/design/jseasy/95100004.asp?x=48&y=10&page=2 – wigging Jan 23 '13 at 19:10
  • I may not be understanding, but it seems that there are a collection of fixed conversion factors. As I'm typing into the "feet" text field, the conversion factor for converting the number (of feet) that I've entered into kilometers is fixed. This is reflected in this line `[self setKilometer: [self foot] * 0.0003048 ]` of your code. In the code I've suggested, typing into the "feet" text field (repeatedly) calls `-setFeet`. On account of `+keyPathsForValuesAffectingKilometers` returning the set containing `@"feet"`, the "kilometers" text field will call `-kilometers` and alter its contents. – Nate Chandler Jan 23 '13 at 19:16
  • As I'm typing into the "kilometers" text field, the conversion factor for converting the number (of kilometers) that I've entered into **miles** is fixed. When `-setKilometers` is called, `-setFeet` will be called with `kilometersToFeet * kilometers` as the argument. Again, since `+keyPathsForValuesAffectingMiles` returns the set containing `@"feet"`, at this point the "miles" text field will call `-miles` and update its contents. `-miles` will return `self.feet * feetToMiles`. – Nate Chandler Jan 23 '13 at 19:20
  • But we just set `self.feet` to `kilometersToFeet * kilometers`. Consequently, `-miles` returns `kilometers * kilometersToFeet * feetToMiles`. While the factor converting kilometers to miles is computed every time this happens (by multiplying `kilometersToFeet` by `feetToMiles`), it seems like this will be good enough. Do you need more precision than this? – Nate Chandler Jan 23 '13 at 19:21
  • If this is precise enough, though, the basic idea is that all you need to do is convert other units to and from one fixed unit, feet for example. – Nate Chandler Jan 23 '13 at 19:24
  • 1
    I have created a program that converts other units from one fixed unit but the problem I had was it wasn't accurate enough. When you use one fixed unit you have the same conversion factors, feet -> conversion factors -> other units. This works fine for units within the same system, like converting kilometers -> meters or miles -> inches but looses accuracy when converting across systems like kilometers -> miles. That's why I used the approach posted in my question because it kept the accuracy, i.e. kilometers -> factorsA -> other units, feet -> factorsB -> other units, etc. – wigging Jan 23 '13 at 19:34
  • If this is insufficiently precise, you can use backing properties (`primitiveMiles`, `primitiveKilometers` and so on) each of which is set with the correct conversion factor when any of the bound properties (`miles`, `kilometers`, etc.) are changed. Each bound property (`feet`, for example) will have its backing property (`primitiveFeet` for `feet) as a key path which affects its value. I'll edit my answer in a moment. – Nate Chandler Jan 23 '13 at 19:35
  • And how would you configure your bindings for the text field in Interface Builder, screen shot would help – wigging Jan 23 '13 at 20:09
  • The same bindings as before. Bind the top text field to `kilometers`, the middle to `miles` and the bottom to `feet`. – Nate Chandler Jan 23 '13 at 20:15
  • Thanks again for all the help. I'm gonna go with the answer posted above since it's shorter and easier to deal with when a lot of units are introduced. – wigging Jan 23 '13 at 21:24
  • Also, I noticed you're in Atlanta, if you're ever in the Greenville area you should stop by one of our programming meet ups. More info at the MeetUp site here http://www.meetup.com/greenvillecocoa/ – wigging Jan 23 '13 at 21:26
  • I'll have to make it down there for one! – Nate Chandler Jan 23 '13 at 21:35