Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do you determine if an object (self) with a lot of properties has been changed?

The short version of the question:
I have a class with a ton of declared properties, and I want to keep track of whether or not there have been any changes to it so that when I call a save method on it, it doesn't write to the database when it isn't needed. How do I update an isDirty property without writing custom setters for all of the declared properties?

The longer version of the question:
Let's say that I have a class like this:

@interface MyObject : NSObject
{
@property (nonatomic, retain) NSString *myString;
@property (nonatomic, assign) BOOL     myBool;
// ... LOTS more properties
@property (nonatomic, assign) BOOL     isDirty;
}

...

@implementation MyObject
{
@synthesize myString;
@synthesize myBool;
// ... LOTS more synthesizes :)
@synthesize isDirty;
}

Attempt 1
My first thought was to implement setValue:forKey: like this:

- (void)setValue:(id)value forKey:(NSString *)key {
    if (![key isEqualToString:@"isDirty"]) {
        if ([self valueForKey:key] != value) {
            if (![[self valueForKey:key] isEqual:value]) {
                self.isDirty = YES;
            }
        }
    }
    [super setValue:value forKey:key];
}

This works perfectly until you set the value directly with a setter (i.e. myObject.myString = @"new string";), in which case setValue:forKey: isn't called (I'm not sure why I thought that it would be, lol).

Attempt 2
Observe all properties of self.

- (id)init
{
    // Normal init stuff
    // Start observing all properties of self
}

- (void)dealloc
{
    // Stop observing all properties of self
}

- (void)observeValueForKeyPath:(NSString *)keyPath 
                      ofObject:(id)object 
                        change:(NSDictionary *)change 
                       context:(void *)context
{
    // set isDirty to true
}  

I'm pretty sure that this will work, but I think that there must be a better way. :) I also want this to be automatic, so that I don't have to maintain a list of properties to watch. I can easily see forgetting to add a property to the list when maintaining this down the road and then trying to figure out why my object sometimes doesn't get saved.

Hopefully I'm overlooking a much simpler approach to this problem!

Final Solution
See my answer below for my final solution to this. It is based on the answer provided by Josh Caswell, but is a working example.

like image 266
lnafziger Avatar asked Jul 09 '12 15:07

lnafziger


1 Answers

A little introspection should help out here. The runtime functions can give you a list of all the object's properties. You can then use those to tell KVO that dirty is dependent on that list. This avoids the maintainability problem of having to update the list of properties by hand. The one caveat is that, like any other solution involving KVO, you won't be notified if the ivar is changed directly -- all access must be through setter methods.

Register to observe self's dirty key path in init, and add this method, creating and returning an NSSet with the names of all the class's properties (except @"dirty", of course).

#import <objc/runtime.h>

+ (NSSet *)keyPathsForValuesAffectingDirty 
{
    unsigned int num_props;
    objc_property_t * prop_list;
    prop_list = class_copyPropertyList(self, &num_props);

    NSMutableSet * propSet = [NSMutableSet set];
    for( unsigned int i = 0; i < num_props; i++ ){
        NSString * propName = [NSString stringWithFormat:@"%s", property_getName(prop_list[i])];
        if( [propName isEqualToString:@"dirty"] ){
            continue;
        }
        [propSet addObject:propName];
    }
    free(prop_list);

    return propSet;
}

Now an observation of dirty will be triggered whenever any of this class's properties are set. (Note that properties defined in superclasses are not included in that list.)

You could instead use that list to register as an observer for all the names individually.

like image 51
jscs Avatar answered Nov 04 '22 15:11

jscs