问题
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.
回答1:
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.
回答2:
It may be a bit overkill depending on your needs, but CoreData provides everything's needed to manage object states and changes. You can use a memory based data store if you do not want to deal with files, but the most powerful setup uses SQLite.
So then, your objects (based on NSManagedObject) will inherit a handful of useful methods, like -changedValues
which lists the changed attributes since the last commit or -committedValuesForKeys: nil
which returns the last committed attributes.
Overkill possibly, but you do not have to reinvent the wheel, you do not need to use a third party library, and it will need only a few lines of code to make it work nicely. Memory usage will be impacted quite a fair bit, but not necessarily for the bad if you choose to use a SQLite datastore.
Core Data apart, using KVO is the way to go to implement your own snapshot mechanism or change manager.
回答3:
My final solution (Thanks Josh Caswell for the example!):
- (id)init
{
if (self = [super init])
{
[self addObserver:self forKeyPath:@"isDirty" options:0 context:NULL];
}
return self;
}
- (void)dealloc
{
[self removeObserver:self forKeyPath:@"isDirty"];
}
- (BOOL)loadData
{
// Load the data, then if successful:
isDirty = NO;
return YES;
}
- (BOOL)saveData
{
if (!self.isDirty)
{
return YES;
}
// Save the data, then if successful:
isDirty = NO;
return YES;
}
// isDirty is dependant on ALL of our declared property.
+ (NSSet *)keyPathsForValuesAffectingIsDirty
{
unsigned int num_props;
objc_property_t *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:@"isDirty"] )
{
[propSet addObject:propName];
}
}
free(prop_list);
return propSet;
}
// If any of our declared properties are changed, this will be called so set isDirty to true.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"isDirty"])
{
isDirty = YES;
}
}
回答4:
I don't know what all of your properties are, but you could try "superclassing" them. Create an object ObservedObject
and then make custom classes for all of your objects that are subclasses of this object. Then either put an isDirty
property on ObservedObject
and look at it, or send a notification to your program when it is changed. This might be a lot of work if you have many different types of objects, but if you have mostly many of the same object it shouldn't be too bad.
I'm interested to see if this is a viable solution or if a good solution can be found for this kind of problem.
回答5:
One option would be in your save method to get the old version myObject and do something like
if (![myOldObject isEqual:myNewObject]) {
//perform save
}
来源:https://stackoverflow.com/questions/11398684/how-do-you-determine-if-an-object-self-with-a-lot-of-properties-has-been-chang