6
votes

My model classes are mostly implemented with synthesized setter/getter methods and everything was fine. Everything was nicely hooked up to the user interface. I later realized that changing one property should cause other properties to change (changing type might cause changes in minA and maxA) so I manually wrote setter/getter methods for the type property. Code follows:

QBElementType

@interface QBElementType : NSObject
   @property NSRange minRange;
   @property NSRange maxRange;
@end

@implementation QBElementType
   @synthesize minRange;
   @synthesize maxRange;
@end

QBElement

@interface QBElement : QBElementType{
    QBElementType *_type;
}
  @property (weak) QBElementType *type;
  @property NSUInteger minA;
  @property NSUInteger maxA;
@end


@implementation QBElement
  @synthesize minA = _minA;
  @synthesize maxA = _maxA;

- (QBElementType*)type
{
    return _type;
}
- (void)setType:(QBElementType*)newType
{
    [self willChangeValueForKey:@"type"];
    _type = newType;   // no need to bother with retain/release due to ARC.
    [self didChangeValueForKey:@"type"];

    /* Having the following 4 lines commented out or not changes nothing to the error
    if (!NSLocationInRange(_minA, newType.minRange))
        self.minA = newType.minRange.location;
    if (!NSLocationInRange(_maxA, newType.maxRange))
        self.maxA = newType.maxRange.location;
    */
}
@end

QUESTION: Since then, whenever I change an Element's Type I get an uncaught exception:

Cannot update for observer <NSTableBinder...> for the key path "type.minRange" from <QBElement...>, most likely because the value for the key "type" was changed without an appropriate KVO notification being sent. Check KVO-Compliance of the QBElement class.

Is there something obvious I'm missing from KVO-Compliance? Why do I get this error?

3

3 Answers

3
votes

You don't need to explicitly call -willChangeValueForKey: and -didChangeValueForKey: because your setter is named correctly. The calls to them will be added automatically through the magic of KVC/KVO's machinery. The problem may be that they're effectively being called twice.

Also, since it appears that minA and maxA are simply derived from the type, you can make them readonly and tell KVO to automatically notify observers that minA and maxA have changed any time type changes:

@interface QBElement : QBElementType{
    QBElementType *_type;
}
  @property (weak) QBElementType *type;
  @property (readonly) NSUInteger minA;
  @property (readonly) NSUInteger maxA;
@end

@implementation QBElement

+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"minA"] ||
        [key isEqualToString:@"maxA"]) {
        keyPaths = [keyPaths setByAddingObject:@"type"];
    }

    return keyPaths;
}

@synthesize type;

- (NSUInteger)minA
{
    return self.type.minRange.location;
}

- (NSUInteger)maxA
{
    return self.type.maxRange.location;
}

@end
2
votes

Implementing a manual setter does not mean you necessarily need to implement manual KVO notifications. You only absolutely need those if you're updating the backing ivar for a property without going through the setter. KVO will automatically replace your setters with versions that call the will/didChange methods before and after the "real" setter is called. If you want to call these yourself in your setter (e.g. you need more precise control over when they're called) you need to override the method +automaticallyNotifiesObserversForKey: to return NO for your type key.

I assume the reason why you're getting an error right now is because the KVO machinery sees that you've called [self willChangeValueForKey:@"type"] twice in a row (once via KVO "magic", once manually) without calling [self didChangeValueForKey:@"type"].

0
votes

As others have noted, you can ditch the willChangeValueForKey:, didChangeValueForKey: calls and implement:

+ (NSSet *) keyPathsForValuesAffectingMinA
{
    return [NSSet setWithObject:@"type"];
}

Same for minB, of course.

Then make minA and minB read-only properties.

The different ways to do it are largely stylistic preferences, though.