Observing a Change to ANY Class Property in Objective-C
Put simply, is there a way to receive a general notification when any property in an Objective-C class is changed? I know I can use KVO to monitor particular property changes, but I have the need to call a particular method whenever any setProperty:
message is sent to my class. I want to be able to receive a generic notification without any concern about which proper开发者_JAVA百科ty in particular was modified.
If it helps to clarify why I want to do this, I am making use of some fast table scrolling code found here: http://blog.atebits.com/2008/12/fast-scrolling-in-tweetie-with-uitableview/
Part of the process of accomplishing this is that whenever a property in a table view cell is modified, [ self setNeedsDisplay ]
needs to be called. I'd rather not have to override the setter methods for every property in my class just to make this call.
As Chuck notes, you can create a dependent key, or of course you can directly observe all the properties (which is less work than overloading the setters).
Using the Objective-C runtime, if you exclusively use properties, you can automate this process using class_copyPropertyList()
. But I'd probably only do this if this problem comes up a bit for you. If you only have one instance of this problem, it's probably easier and safer and more maintainable just to directly observe the list of properties unless you feel like working in the ObjC runtime.
Here's an example built off of Chuck and Rob's suggestions:
DrakeObject.h
@interface DrakeObject : NSObject
@property (nonatomic, strong) NSNumber *age;
@property (nonatomic, strong) NSNumber *money;
@property (nonatomic, strong) NSString *startPosition;
@property (nonatomic, strong) NSString *currentPosition;
@property (nonatomic, strong, readonly) id propertiesChanged;
@end
DrakeObject.m
@implementation DrakeObject
- (instancetype)init {
self = [super init];
if (self) {
self.age = @25;
self.money = @25000000;
self.startPosition = @"bottom";
self.currentPosition = @"here";
}
return self;
}
- (id)propertiesChanged {
return nil;
}
+(NSSet *)keyPathsForValuesAffectingPropertiesChanged {
return [NSSet setWithObjects:@"age", @"money", @"startPosition", @"currentPosition", nil];
}
observing propertiesChanged will let us know anytime a property has changed.
[self.drakeObject addObserver:self
forKeyPath:@"propertiesChanged"
options:NSKeyValueObservingOptionNew
context:nil];
Not exactly. You can create a dependent key that depends on every property you wish to expose and then observe that. That's about as close as you'll get, I think.
Here an example of code. I have a general object and dother object. Dother object has to save his state on change each property.
#import <Foundation/Foundation.h>
@interface GeneralObject : NSObject
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary;
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary;
- (NSDictionary *)dictionaryValue;
- (NSArray *)allPropertyNames;
@end
implementation
#import "GeneralObject.h"
#import <objc/runtime.h>
@implementation GeneralObject
#pragma mark - Public
+ (instancetype)instanceWithDictionary:(NSDictionary *)aDictionary {
return [[self alloc] initWithDictionary:aDictionary];
}
- (instancetype)initWithDictionary:(NSDictionary *)aDictionary {
aDictionary = [aDictionary clean];
for (NSString* propName in [self allPropertyNames]) {
[self setValue:aDictionary[propName] forKey:propName];
}
return self;
}
- (NSDictionary *)dictionaryValue {
NSMutableDictionary *result = [NSMutableDictionary dictionary];
NSArray *propertyNames = [self allPropertyNames];
id object;
for (NSString *key in propertyNames) {
object = [self valueForKey:key];
if (object) {
[result setObject:object forKey:key];
}
}
return result;
}
- (NSArray *)allPropertyNames {
unsigned count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
NSMutableArray *array = [NSMutableArray array];
unsigned i;
for (i = 0; i < count; i++) {
objc_property_t property = properties[i];
NSString *name = [NSString stringWithUTF8String:property_getName(property)];
[array addObject:name];
}
free(properties);
return array;
}
@end
and after all we have dother class, which should save his state on each change of any property
#import "GeneralObject.h"
extern NSString *const kUserDefaultsUserKey;
@interface DotherObject : GeneralObject
@property (strong, nonatomic) NSString *firstName;
@property (strong, nonatomic) NSString *lastName;
@property (strong, nonatomic) NSString *email;
@end
and implementation
#import "DotherObject.h"
NSString *const kUserDefaultsUserKey = @"CurrentUserKey";
@implementation DotherObject
- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
if (self = [super initWithDictionary:dictionary]) {
for (NSString *key in [self allPropertyNames]) {
[self addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
}
}
return self;
}
- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context {
NSDictionary *dict = [self dictionaryValue];
[[NSUserDefaults standardUserDefaults] setObject:dict forKey:kUserDefaultsUserKey];
[[NSUserDefaults standardUserDefaults] synchronize];
}
- (NSString *)description {
return [NSString stringWithFormat:@"%@; dict:\n%@", [super description], [self dictionaryValue]];
}
@end
Happy coding!
精彩评论