Search code examples
objective-ccocoakey-value-observingnstextfield

KVO causes loop if observers to other objects are not removed


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...


Solution

  • 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
    }