Search code examples
objective-cmacoscocoakey-value-observing

Why do I get an uncaught exception when implementing my own KVC setter/getter methods


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?


Solution

  • 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