diff --git a/Source/NSKVOInternal.h b/Source/NSKVOInternal.h index 7748f0def..de762d5c0 100644 --- a/Source/NSKVOInternal.h +++ b/Source/NSKVOInternal.h @@ -123,6 +123,7 @@ _NSKVOEnsureKeyWillNotify(id object, NSString *key); */ @interface NSObject (NSKeyValueObservingPrivate) +- (Class)_underlyingClass; - (void)_notifyObserversOfChangeForKey:(NSString *)key oldValue:(id)oldValue newValue:(id)newValue; diff --git a/Source/NSKVOSupport.m b/Source/NSKVOSupport.m index 9dd31562c..822f3fcc4 100644 --- a/Source/NSKVOSupport.m +++ b/Source/NSKVOSupport.m @@ -333,7 +333,11 @@ - (bool) isEmpty // Aggregate all keys whose values will affect us. if (dependents) { - Class cls = [object class]; + // Make sure to retrieve the underlying class of the observee. + // This is just [object class] for an NSObject derived class. + // When observing an object through a proxy, we instead use KVC + // to optain the underlying class. + Class cls = [object _underlyingClass]; NSSet *valueInfluencingKeys = [cls keyPathsForValuesAffectingValueForKey: key]; if (valueInfluencingKeys.count > 0) { @@ -1123,6 +1127,11 @@ - (void)didChangeValueForKey: (NSString *)key @implementation NSObject (NSKeyValueObservingPrivate) +- (Class)_underlyingClass +{ + return [self class]; +} + - (void)_notifyObserversOfChangeForKey: (NSString *)key oldValue: (id)oldValue newValue: (id)newValue @@ -1254,3 +1263,19 @@ - (void)removeObserver: (id)observer forKeyPath:(NSString *)keyPath @end #pragma endregion + +#pragma region KVO forwarding - NSProxy category + +@implementation +NSProxy (NSKeyValueObserving) + +- (Class)_underlyingClass +{ + // Retrieve the underlying class via KVC + // Note that we assume that the class is KVC-compliant, when KVO is used + return [(NSObject *)self valueForKey: @"class"]; +} + +@end + +#pragma endregion diff --git a/Source/NSKVOSwizzling.m b/Source/NSKVOSwizzling.m index 5b9965baa..41ebd77e1 100644 --- a/Source/NSKVOSwizzling.m +++ b/Source/NSKVOSwizzling.m @@ -666,6 +666,8 @@ static void funcName(id self, SEL _cmd, type val) \ _NSKVOEnsureKeyWillNotify(id object, NSString *key) { char *rawKey; + Class cls; + Class underlyingCls; // Since we cannot replace the isa of tagged pointer objects, we can't swizzle // them. @@ -674,8 +676,17 @@ static void funcName(id self, SEL _cmd, type val) \ return; } + cls = [object class]; + underlyingCls = [object _underlyingClass]; + // If cls differs from underlyingCls, object is actually a proxy. + // Retrieve the underlying object with KVC. + if (cls != underlyingCls) + { + object = [object valueForKey: @"self"]; + } + // A class is allowed to decline automatic swizzling for any/all of its keys. - if (![[object class] automaticallyNotifiesObserversForKey: key]) + if (![underlyingCls automaticallyNotifiesObserversForKey: key]) { return; } diff --git a/Tests/base/NSKVOSupport/proxy.m b/Tests/base/NSKVOSupport/proxy.m index 30a9dffa1..3aa1a3b82 100644 --- a/Tests/base/NSKVOSupport/proxy.m +++ b/Tests/base/NSKVOSupport/proxy.m @@ -96,9 +96,41 @@ - (void) dealloc @end +@interface Wrapper : NSObject +{ + TProxy *_proxy; +} + +- (instancetype) initWithProxy: (TProxy *) proxy; + +- (TProxy *) proxy; + +@end + +@implementation Wrapper + +- (instancetype) initWithProxy: (TProxy *) proxy +{ + self = [super init]; + if (self) + { + _proxy = proxy; + } + + return self; +} + +- (TProxy *) proxy +{ + return _proxy; +} + +@end + @interface Observer: NSObject { int count; + NSArray *keys; } - (void)runTest; @@ -107,10 +139,13 @@ - (void)runTest; @implementation Observer -- (void)runTest +- (void)simpleKeypathTest { Observee *obj = [[Observee alloc] init]; TProxy *proxy = [[TProxy alloc] initWithProxiedObject:obj]; + + keys = [NSArray arrayWithObjects: @"derivedName", @"name", nil]; + count = 0; [(Observee *)proxy addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:NULL]; [(Observee *)proxy addObserver:self forKeyPath:@"derivedName" options:NSKeyValueObservingOptionNew context:NULL]; @@ -128,21 +163,50 @@ - (void)runTest [obj release]; } +- (void)nestedKeypathTest +{ + Observee *obj = [[Observee alloc] init]; + TProxy *proxy = [[TProxy alloc] initWithProxiedObject:obj]; + Wrapper *w = [[Wrapper alloc] initWithProxy: proxy]; + + keys = [NSArray arrayWithObjects: @"proxy.derivedName", @"proxy.name", nil]; + count = 0; + + [w addObserver:self forKeyPath:@"proxy.name" options:NSKeyValueObservingOptionNew context:NULL]; + [w addObserver:self forKeyPath:@"proxy.derivedName" options:NSKeyValueObservingOptionNew context:NULL]; + + [((Observee *)proxy) setName: @"MOO"]; + PASS(count == 2, "Got two change notifications"); + + [obj setName: @"BAH"]; + PASS(count == 4, "Got two change notifications"); + + [w removeObserver:self forKeyPath:@"proxy.name" context:NULL]; + [w removeObserver:self forKeyPath:@"proxy.derivedName" context:NULL]; + + [w release]; + [proxy release]; + [obj release]; + + count = 0; + +} + - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { count += 1; switch (count) { case 1: - PASS_EQUAL(keyPath, @"derivedName", "change notification for dependent key 'derivedName' is emitted first"); + PASS_EQUAL(keyPath, [keys objectAtIndex: 0], "change notification for dependent key 'derivedName' is emitted first"); break; case 2: - PASS_EQUAL(keyPath, @"name", "'name' change notification for proxy is second"); + PASS_EQUAL(keyPath, [keys objectAtIndex: 1], "'name' change notification for proxy is second"); break; case 3: - PASS_EQUAL(keyPath, @"derivedName", "'derivedName' change notification for object is third"); + PASS_EQUAL(keyPath, [keys objectAtIndex: 0], "'derivedName' change notification for object is third"); break; case 4: - PASS_EQUAL(keyPath, @"name", "'name' change notification for object is fourth"); + PASS_EQUAL(keyPath, [keys objectAtIndex: 1], "'name' change notification for object is fourth"); break; default: PASS(0, "unexpected -[Observer observeValueForKeyPath:ofObject:change:context:] callback"); @@ -154,15 +218,15 @@ - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(N int main(int argc, char *argv[]) { - NSAutoreleasePool *arp = [NSAutoreleasePool new]; + START_SET("KVO Proxy Tests") Observer *obs = [Observer new]; testHopeful = YES; - [obs runTest]; + [obs simpleKeypathTest]; + [obs nestedKeypathTest]; testHopeful = NO; [obs release]; - - DESTROY(arp); + END_SET("KVO Proxy Tests") return 0; }