Search code examples
objective-ccore-datakey-value-observing

avoid unnecessary KVO update for dependent key


I have a Core Data entity that I want to store a CGRect value. Because there is no CGRect support for Core Data so I just split it into four float attributes: x, y, width, height.

In order to make it more convenient for me, I add a custom property frame that return the combined CGRect from the four float attributes. BTW, I am using mogenerator.

@interface ViewableData : _ViewableData

@property (nonatomic) CGRect frame;

@end

@implementation ViewableData

- (void)setFrame:(CGRect)frame {
    if (frame.origin.x != self.xValue)
        self.xValue = frame.origin.x;
    if (frame.origin.y != self.yValue)
        self.yValue = frame.origin.y;
    if (frame.size.width != self.widthValue)
        self.widthValue = frame.size.width;
    if (frame.size.height != self.heightValue)
        self.heightValue = frame.size.height;
}

- (CGRect)frame {
    return CGRectMake(self.xValue, self.yValue, self.widthValue, self.heightValue);
}

+ (NSSet *)keyPathsForValuesAffectingFrame {
    return [NSSet setWithObjects:ViewableDataAttributes.height, ViewableDataAttributes.width, ViewableDataAttributes.x, ViewableDataAttributes.y, nil];
}

@end

It works as I expected but I found that KVO observer get notified too many times than it needs. Set a frame may notifier the observer 4 times. Is anyway to avoid the unnecessary KVO update notification?


Edit

Looks Transformable type is a good solution, but I still want to know a proper way to update depend key path. Another example is haveinga property fullName that depend on firstName and lastName. Still, if I change fullName I only expect the observer only get notified once instead of twice (once with firstName changed and once with lastName changed). Another problem is that at the first notification, the state is inconsistent because only partially update is performed.

I think my original example is not good enough. How about this:

@interface Person : NSManagedObject

@property (copy, nonatomic) NSString fullName;
@property (copy, nonatomic) NSString firstName;
@property (copy, nonatomic) NSString lastName;    

@end

@implementation Person
@dynamic firstName;
@dynamic lastName;

- (void)setFullName:(NSString *)fullName {
    NSArray *names = [fullName componentsSeparatedByString:@" "];
    self.firstName = names[0];
    self.lastName = names[1];
}

- (CGRect)fullName {
    return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
}

+ (NSSet *)keyPathsForValuesAffectingFrame {
    return [NSSet setWithObjects:@"firstName", @"lastName", nil];
}

@end

Then if i do

person = // get Person object
person.firstName = @"A";
person.lastName = @"B";
// person.fullName is now "A B"

person.fullName = @"C D";

On the last line, the observer will see fist name updated (with full name of "C B", but such full name does not exist at all, which may crash my app) than last name updated ("C D")


Solution

  • Regarding your second question... imho you need to think it more carefully. What if you have an observer on firstName only and you update fullName? Do you want that the observer is notified or not?

    BTW: I think that you should use setPrimitiveValue:forKey: and primitiveValueForKey: and manually fire the notifications..

    E.g.

    - (void)setFullName:(NSString *)fullName {
        NSArray *names = [fullName componentsSeparatedByString:@" "];
        [self willChangeValueForKey:@"fullName"];
        [self setPrimitiveValue:names[0] forKey:@"firstName"];
        [self setPrimitiveValue:names[1] forKey:@"lastName"];
        [self didChangeValueForKey:@"fullName"];
    }
    

    In this way you fire only the notification for fullName, but not for firstName and lastName

    EDIT: ok.. regarding your first question (CGRect), if you don't want to use a transformable (even if this is one of their common use) and you can "enforce" that the modification to the rect is made as a whole (that is you never call xValue, etc.. directl, you can write your setFrame as:

    - (void)setFrame:(CGRect)frame {
        [self willChangeValueForKey:@"frame"];
        if (frame.origin.x != self.xValue)
            [self setPrimitiveValue:frame.origin.x forKey:@"xValue"];
        if (frame.origin.y != self.yValue)
            [self setPrimitiveValue:frame.origin.y forKey:@"yValue"];
        if (frame.size.width != self.widthValue)
            [self setPrimitiveValue:frame.size.width forKey:@"widthValue"];
        if (frame.size.height != self.heightValue)
            [self setPrimitiveValue:frame.size.height forKey:@"heightValue"];
        [self didChangeValueForKey:@"frame"];
    }
    

    EDIT2: Regarding your "fullName" problem... I would create a Person object with only firstName and lastName, and then a category

    #import "Person.h"
    
    @interface Person (FullName)
    @property (nonatomic, strong) NSString* fullName;
    @end
    
    //.m
    #import "Person+FullName.h"
    
    @implementation Person (FullName)
    - (NSString*)fullName
    {
       return [NSString stringWithFormat:@"%@ %@", self.firstName, self.lastName];
    }
    
    - (void)setFullName:(NSString*)fullName
    {
        NSArray *names = [fullName componentsSeparatedByString:@" "];
        [self willChangeValueForKey:@"fullName"];
        [self willChangeValueForKey:@"firstName"];
        [self willChangeValueForKey:@"lastName"];
        [self setPrimitiveValue:names[0] forKey:@"firstName"];
        [self setPrimitiveValue:names[1] forKey:@"lastName"];
        [self didChangeValueForKey:@"lastName"];
        [self didChangeValueForKey:@"firstName"];
        [self didChangeValueForKey:@"fullName"];
    } 
    @end