https://github.com/farcaller/cocotron/blob/af740de86c9bee84c59ffc74d27e5df9e22e1391/Foundation/NSKeyValueCoding/NSKeyValueObserving.m
/* Copyright (c) 2007-2008 Johannes Fortmann | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ | |
#import <Foundation/NSKeyValueObserving.h> | |
#import <Foundation/NSArray.h> | |
#import <Foundation/NSSet.h> | |
#import <Foundation/NSDictionary.h> | |
#import <Foundation/NSLock.h> | |
#import <Foundation/NSValue.h> | |
#import <Foundation/NSException.h> | |
#import <Foundation/NSEnumerator.h> | |
#import <Foundation/NSKeyValueCoding.h> | |
#import <Foundation/NSMethodSignature.h> | |
#import <Foundation/NSIndexSet.h> | |
#import <Foundation/NSDebug.h> | |
#import <objc/objc-runtime.h> | |
#import <objc/objc-class.h> | |
#import <string.h> | |
#import <ctype.h> | |
#import "NSString+KVCAdditions.h" | |
#import "NSKeyValueObserving-Private.h" | |
NSString *const NSKeyValueChangeKindKey=@"NSKeyValueChangeKindKey"; | |
NSString *const NSKeyValueChangeNewKey=@"NSKeyValueChangeNewKey"; | |
NSString *const NSKeyValueChangeOldKey=@"NSKeyValueChangeOldKey"; | |
NSString *const NSKeyValueChangeIndexesKey=@"NSKeyValueChangeIndexesKey"; | |
NSString *const NSKeyValueChangeNotificationIsPriorKey=@"NSKeyValueChangeNotificationIsPriorKey"; | |
NSString *const _KVO_DependentKeysTriggeringChangeNotification=@"_KVO_DependentKeysTriggeringChangeNotification"; | |
NSString *const _KVO_KeyPathsForValuesAffectingValueForKey=@"_KVO_KeyPathsForValuesAffectingValueForKey"; | |
#pragma mark - | |
#pragma mark KVO implementation | |
static NSMutableDictionary *observationInfos=nil; | |
static NSLock *kvoLock=nil; | |
@interface NSObject (KVOSettersForwardReferencs) | |
+(void)_KVO_buildDependencyUnion; | |
@end | |
@interface NSObject (KVCPrivateMethod) | |
-(void)_demangleTypeEncoding:(const char*)type to:(char*)cleanType; | |
@end | |
@implementation NSObject (KeyValueObserving) | |
-(void*)observationInfo | |
{ | |
return [[observationInfos objectForKey:[NSValue valueWithPointer:self]] pointerValue]; | |
} | |
-(void)setObservationInfo:(void*)info | |
{ | |
if(!observationInfos) | |
observationInfos=[NSMutableDictionary new]; | |
[observationInfos setObject:[NSValue valueWithPointer:info] forKey:[NSValue valueWithPointer:self]]; | |
} | |
+(void*)observationInfo | |
{ | |
return [[observationInfos objectForKey:[NSValue valueWithPointer:self]] pointerValue]; | |
} | |
+(void)setObservationInfo:(void*)info | |
{ | |
if(!observationInfos) | |
observationInfos=[NSMutableDictionary new]; | |
[observationInfos setObject:[NSValue valueWithPointer:info] forKey:[NSValue valueWithPointer:self]]; | |
} | |
-(void)addObserver:(id)observer forKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options context:(void*)context; | |
{ | |
[self _KVO_swizzle]; | |
NSString* remainingKeyPath; | |
NSString* key; | |
[keyPath _KVC_partBeforeDot:&key afterDot:&remainingKeyPath]; | |
if([keyPath hasPrefix:@"@"]) { | |
// the key path is an operator: don't evaluate | |
key=keyPath; | |
remainingKeyPath=nil; | |
} | |
// get observation info dictionary | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
// get all observers for current key | |
NSMutableArray *observers=[observationInfo objectForKey:key]; | |
// find if already observing | |
_NSObservationInfo *oldInfo=nil; | |
for(_NSObservationInfo *current in observers) { | |
if(current->observer==observer && | |
[[current keyPath] isEqualToString:keyPath]) { | |
oldInfo=current; | |
break; | |
} | |
} | |
// create new info | |
_NSObservationInfo *info=nil; | |
if(oldInfo) | |
info=oldInfo; | |
else | |
info=[[_NSObservationInfo new] autorelease]; | |
// We now add the observer to the rest of the path, then to the dependents. | |
// Any of the following may fail if a key path doesn't exist. | |
// We have to keep track of how far we have come to be able to roll back. | |
id lastPathTried=nil; | |
NSSet* dependentPathsForKey=[isa keyPathsForValuesAffectingValueForKey:key]; | |
@try { | |
// if observing a key path, also observe all deeper levels | |
// info object acts as a proxy replacing remainingKeyPath with keyPath | |
if([remainingKeyPath length]) | |
{ | |
lastPathTried=remainingKeyPath; | |
[[self valueForKey:key] addObserver:info | |
forKeyPath:remainingKeyPath | |
options:options | |
context:context]; | |
} | |
// now try all dependent key paths | |
for(NSString *path in dependentPathsForKey) | |
{ | |
lastPathTried=path; | |
[self addObserver:info | |
forKeyPath:path | |
options:options | |
context:context]; | |
} | |
} | |
@catch(id ex) { | |
// something went wrong. rollback all the work we've done so far. | |
BOOL wasInDependentKey=NO; | |
if(lastPathTried!=remainingKeyPath) { | |
wasInDependentKey=YES; | |
// adding to a dependent path failed. roll back for all the paths before. | |
if([remainingKeyPath length]) { | |
[[self valueForKey:key] removeObserver:info forKeyPath:remainingKeyPath]; | |
} | |
for(NSString *path in dependentPathsForKey) | |
{ | |
if(path==lastPathTried) | |
break; // this is the one that failed | |
[self removeObserver:info forKeyPath:path]; | |
} | |
} | |
// reformat exceptions to be more expressive | |
if([[ex name] isEqualToString:NSUndefinedKeyException]) { | |
if(!wasInDependentKey) { | |
[NSException raise:NSUndefinedKeyException | |
format:@"Undefined key while adding observer for key path %@ to object %p", keyPath, self]; | |
} | |
else { | |
[NSException raise:NSUndefinedKeyException | |
format:@"Undefined key while adding observer for dependent key path %@ of path %@ to object %p", lastPathTried, keyPath, self]; | |
} | |
} | |
[ex raise]; | |
} | |
// we were able to observe the full length of our key path, and the dependents. | |
// now make the changes to our own observers array | |
// create observation info dictionary if it's not there | |
// (we have to re-check: it may have been created while observing our dependents | |
if(!observationInfo && !(observationInfo = [self observationInfo])) | |
{ | |
[self setObservationInfo:[NSMutableDictionary new]]; | |
observationInfo=[self observationInfo]; | |
observers = [observationInfo objectForKey:key]; | |
} | |
// get all observers for current key | |
if(!observers) | |
{ | |
observers = [NSMutableArray array]; | |
[observationInfo setObject:observers forKey:key]; | |
} | |
// set info options | |
info->observer=observer; | |
info->options=options; | |
info->context=context; | |
info->object=self; | |
[info setKeyPath:keyPath]; | |
if(!oldInfo) { | |
[observers addObject:info]; | |
} | |
if(options & NSKeyValueObservingOptionInitial) | |
{ | |
[self willChangeValueForKey:keyPath]; | |
[self didChangeValueForKey:keyPath]; | |
} | |
} | |
-(void)removeObserver:(id)observer forKeyPath:(NSString*)keyPath; | |
{ | |
NSString* key, *remainingKeyPath; | |
[keyPath _KVC_partBeforeDot:&key afterDot:&remainingKeyPath]; | |
if([keyPath hasPrefix:@"@"]) { | |
// the key path is an operator: don't evaluate | |
key=keyPath; | |
remainingKeyPath=nil; | |
} | |
// now remove own observer | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
NSMutableArray *observers=[observationInfo objectForKey:key]; | |
for(_NSObservationInfo *info in [[observers copy] autorelease]) | |
{ | |
if(info->observer==observer && | |
[[info keyPath] isEqualToString:keyPath]) | |
{ | |
[[info retain] autorelease]; | |
[observers removeObject:info]; | |
if(![observers count]) | |
{ | |
[observationInfo removeObjectForKey:key]; | |
} | |
if(![observationInfo count]) | |
{ | |
[self setObservationInfo:nil]; | |
[observationInfo release]; | |
} | |
if([remainingKeyPath length]) | |
[[self valueForKey:key] removeObserver:info forKeyPath:remainingKeyPath]; | |
NSSet* keysPathsForKey=[isa keyPathsForValuesAffectingValueForKey:key]; | |
for(NSString *path in keysPathsForKey) | |
{ | |
[self removeObserver:info | |
forKeyPath:path]; | |
} | |
return; | |
} | |
} | |
// 10.4 Apple implementation will crash at this point... | |
[NSException raise:@"NSKVOException" format:@"trying to remove observer %@ for unobserved key path %@", observer, keyPath]; | |
} | |
-(void)willChangeValueForKey:(NSString*)key | |
{ | |
NSMutableDictionary *dict=[NSMutableDictionary new]; | |
[dict setObject:[NSNumber numberWithInt:NSKeyValueChangeSetting] | |
forKey:NSKeyValueChangeKindKey]; | |
[self _willChangeValueForKey:key changeOptions:dict]; | |
[dict release]; | |
} | |
-(void)didChangeValueForKey:(NSString*)key | |
{ | |
[self _didChangeValueForKey:key changeOptions:nil]; | |
} | |
- (void)willChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key | |
{ | |
NSMutableDictionary *dict=[NSMutableDictionary new]; | |
[dict setObject:[NSNumber numberWithUnsignedInteger:change] | |
forKey:NSKeyValueChangeKindKey]; | |
[dict setObject:indexes | |
forKey:NSKeyValueChangeIndexesKey]; | |
[self _willChangeValueForKey:key changeOptions:dict]; | |
[dict release]; | |
} | |
- (void)didChange:(NSKeyValueChange)change valuesAtIndexes:(NSIndexSet *)indexes forKey:(NSString *)key | |
{ | |
[self _didChangeValueForKey:key changeOptions:nil]; | |
} | |
#pragma mark Observer notification | |
-(void)_willChangeValueForKey:(NSString*)key changeOptions:(NSDictionary*)changeOptions | |
{ | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
if(!observationInfo) | |
return; | |
NSMutableArray *observers=[observationInfo objectForKey:key]; | |
for(_NSObservationInfo *info in [[observers copy] autorelease]) | |
{ | |
// increment change count for nested did/willChangeValue's | |
info->willChangeCount++; | |
if(info->willChangeCount>1) | |
continue; | |
NSString* keyPath=info->keyPath; | |
if(![info changeDictionary]) | |
{ | |
id cd=[changeOptions mutableCopy]; | |
[info setChangeDictionary:cd]; | |
[cd release]; | |
} | |
// store old value if applicable | |
if(info->options & NSKeyValueObservingOptionOld) | |
{ | |
id idxs=[info->changeDictionary objectForKey:NSKeyValueChangeIndexesKey]; | |
if(idxs) | |
{ | |
int type=[[info->changeDictionary objectForKey:NSKeyValueChangeKindKey] intValue]; | |
// for to-many relationships, oldvalue is only sensible for replace and remove | |
if(type == NSKeyValueChangeReplacement || | |
type == NSKeyValueChangeRemoval) | |
[info->changeDictionary setValue:[[self mutableArrayValueForKeyPath:keyPath] objectsAtIndexes:idxs] forKey:NSKeyValueChangeOldKey]; | |
} | |
else | |
{ | |
[info->changeDictionary setValue:[self valueForKeyPath:keyPath] forKey:NSKeyValueChangeOldKey]; | |
} | |
} | |
// inform observer of change | |
if(info->options & NSKeyValueObservingOptionPrior) | |
{ | |
[info->changeDictionary setObject:[NSNumber numberWithBool:YES] | |
forKey:NSKeyValueChangeNotificationIsPriorKey]; | |
[info->observer observeValueForKeyPath:info->keyPath | |
ofObject:self | |
change:info->changeDictionary | |
context:info->context]; | |
[info->changeDictionary removeObjectForKey:NSKeyValueChangeNotificationIsPriorKey]; | |
} | |
NSString* firstPart, *rest; | |
[keyPath _KVC_partBeforeDot:&firstPart afterDot:&rest]; | |
if([keyPath hasPrefix:@"@"]) { | |
// the key path is an operator: don't evaluate | |
key=keyPath; | |
rest=nil; | |
} | |
// remove deeper levels (those items will change) | |
if(rest) | |
[[self valueForKey:firstPart] removeObserver:info forKeyPath:rest]; | |
} | |
} | |
-(void)_didChangeValueForKey:(NSString*)key changeOptions:(NSDictionary*)ignored | |
{ | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
if(!observationInfo) | |
return; | |
NSMutableArray *observers=[[observationInfo objectForKey:key] copy]; | |
for(_NSObservationInfo *info in observers) | |
{ | |
// decrement count and only notify after last didChange | |
info->willChangeCount--; | |
if(info->willChangeCount>0) | |
continue; | |
NSString* keyPath=info->keyPath; | |
// store new value if applicable | |
if(info->options & NSKeyValueObservingOptionNew) | |
{ | |
id idxs=[info->changeDictionary objectForKey:NSKeyValueChangeIndexesKey]; | |
if(idxs) | |
{ | |
int type=[[info->changeDictionary objectForKey:NSKeyValueChangeKindKey] intValue]; | |
// for to-many relationships, newvalue is only sensible for replace and insert | |
if(type == NSKeyValueChangeReplacement || | |
type == NSKeyValueChangeInsertion) | |
[info->changeDictionary setValue:[[self mutableArrayValueForKeyPath:keyPath] objectsAtIndexes:idxs] forKey:NSKeyValueChangeNewKey]; | |
} | |
else | |
{ | |
[info->changeDictionary setValue:[self valueForKeyPath:keyPath] forKey:NSKeyValueChangeNewKey]; | |
} | |
} | |
// restore deeper observers if applicable | |
NSString* firstPart, *rest; | |
[keyPath _KVC_partBeforeDot:&firstPart afterDot:&rest]; | |
if([keyPath hasPrefix:@"@"]) { | |
// the key path is an operator: don't evaluate | |
key=keyPath; | |
rest=nil; | |
} | |
if(rest) | |
{ | |
[[self valueForKey:firstPart] | |
addObserver:info | |
forKeyPath:rest | |
options:info->options | |
context:info->context]; | |
} | |
// inform observer of change | |
[info->observer observeValueForKeyPath:info->keyPath | |
ofObject:self | |
change:info->changeDictionary | |
context:info->context]; | |
[info setChangeDictionary:nil]; | |
} | |
[observers release]; | |
} | |
+(void)setKeys:(NSArray *)keys triggerChangeNotificationsForDependentKey:(NSString *)dependentKey | |
{ | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
if(!observationInfo) | |
{ | |
observationInfo=[NSMutableDictionary new]; | |
[self setObservationInfo:observationInfo]; | |
} | |
NSMutableDictionary *dependencies=[observationInfo objectForKey:_KVO_DependentKeysTriggeringChangeNotification]; | |
if(!dependencies) | |
{ | |
dependencies=[NSMutableDictionary dictionary]; | |
[observationInfo setObject:dependencies | |
forKey:_KVO_DependentKeysTriggeringChangeNotification]; | |
} | |
id key; | |
id en=[keys objectEnumerator]; | |
while((key = [en nextObject])) | |
{ | |
NSMutableSet* allDependencies=[dependencies objectForKey:key]; | |
if(!allDependencies) | |
{ | |
allDependencies=[NSMutableSet new]; | |
[dependencies setObject:allDependencies | |
forKey:key]; | |
[allDependencies release]; | |
} | |
[allDependencies addObject:dependentKey]; | |
} | |
} | |
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key | |
{ | |
NSString *methodName=[[NSString alloc] initWithFormat:@"keyPathsForValuesAffecting%@", [key capitalizedString]]; | |
NSSet* ret=nil; | |
SEL sel=NSSelectorFromString(methodName); | |
if([self respondsToSelector:sel]) | |
{ | |
ret=[self performSelector:sel]; | |
} | |
else | |
{ | |
[self _KVO_buildDependencyUnion]; | |
NSMutableDictionary* observationInfo=[self observationInfo]; | |
NSMutableDictionary *keyPathsByKey=[observationInfo objectForKey:_KVO_KeyPathsForValuesAffectingValueForKey]; | |
ret=[keyPathsByKey objectForKey:key]; | |
} | |
[methodName release]; | |
return ret; | |
} | |
@end | |
#pragma mark - | |
#pragma mark KVO-notifying setters and swizzeling code | |
/* The following functions define suitable setters and getters which | |
call willChangeValueForKey: and didChangeValueForKey: on their superclass | |
_KVO_swizzle changes the class of its object to a subclass which overrides | |
each setter with a suitable KVO-Notifying one. | |
*/ | |
// selector for change type | |
#define CHANGE_SELECTOR(type) KVO_notifying_change_ ## type : | |
// definition for change type | |
#define CHANGE_DEFINE(type) -( void ) KVO_notifying_change_ ## type : ( type ) value | |
// original selector called by swizzled selector | |
#define ORIGINAL_SELECTOR(name) NSSelectorFromString([NSString stringWithFormat:@"_original_%@", name]) | |
// declaration of change function: | |
// extracts key from selector called, calls original function | |
#define CHANGE_DECLARATION(type) CHANGE_DEFINE(type) | |
{ | |
const char* origName = sel_getName(_cmd); | |
size_t selLen=strlen(origName); | |
char *sel=__builtin_alloca(selLen+1); | |
strcpy(sel, origName); | |
sel[selLen-1]=' |