KVO causes loop if observers to other objects are not removed

可紊 提交于 2019-12-23 03:16:05

问题


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:

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


回答1:


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
}



回答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;
}


来源:https://stackoverflow.com/questions/14484053/kvo-causes-loop-if-observers-to-other-objects-are-not-removed

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!