Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Key-Value Observing: Add support for observing through a proxy #479

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Source/NSKVOInternal.h
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ _NSKVOEnsureKeyWillNotify(id object, NSString *key);
*/
@interface
NSObject (NSKeyValueObservingPrivate)
- (Class)_underlyingClass;
- (void)_notifyObserversOfChangeForKey:(NSString *)key
oldValue:(id)oldValue
newValue:(id)newValue;
Expand Down
27 changes: 26 additions & 1 deletion Source/NSKVOSupport.m
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
13 changes: 12 additions & 1 deletion Source/NSKVOSwizzling.m
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Expand Down
82 changes: 73 additions & 9 deletions Tests/base/NSKVOSupport/proxy.m
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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];
Expand All @@ -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");
Expand All @@ -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;
}
Loading