Skip to content

Commit

Permalink
Key Value Observing, with test cases.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ross Boucher committed Sep 26, 2008
1 parent 3b39b64 commit 3834751
Show file tree
Hide file tree
Showing 2 changed files with 467 additions and 79 deletions.
231 changes: 165 additions & 66 deletions Foundation/CPKeyValueObserving.j
Expand Up @@ -36,17 +36,17 @@ import "CPObject.j"

}

- addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
{
if (!anObserver || !aPath)
if (!anObserver || !aPath)
return;

[[_CPKVOProxy proxyForObject:self] _addObserver:anObserver forKeyPath:aPath options:options context:aContext];
}

- removeObserver:(id)anObserver forKeyPath:(CPString)aPath
- (void)removeObserver:(id)anObserver forKeyPath:(CPString)aPath
{
if (!anObserver || !aPath)
if (!anObserver || !aPath)
return;

[[KVOProxyMap objectForKey:[self hash]] _removeObserver:anObserver forKeyPath:aPath];
Expand All @@ -65,6 +65,9 @@ CPKeyValueObservingOptionOld = 1 << 1;
CPKeyValueObservingOptionInitial = 1 << 2;
CPKeyValueObservingOptionPrior = 1 << 3;

//convenience
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;

// KVO Change Dictionary Keys
CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey";
CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey";
Expand Down Expand Up @@ -95,9 +98,9 @@ var KVOProxyMap = [CPDictionary dictionary];
return proxy;

proxy = [[self alloc] initWithTarget:anObject];

[proxy _replaceSetters];

anObject.isa = proxy.isa;

[KVOProxyMap setObject:proxy forKey:[anObject hash]];
Expand All @@ -108,33 +111,33 @@ var KVOProxyMap = [CPDictionary dictionary];
- (id)initWithTarget:(id)aTarget
{
self = [super init];

_targetObject = aTarget;
_nativeClass = [aTarget class];
_replacementMethods = [CPDictionary dictionary];
_observersForKey = [CPDictionary dictionary];
_changesForKey = [CPDictionary dictionary];

return self;
}

- (void)_replaceSetters
{
var currentClass = [_targetObject class];

while (currentClass && currentClass != currentClass.super_class)
{
var methodList = currentClass.method_list,
count = methodList.length;

for (var i=0; i<count; i++)
{
var newMethod = _kvoMethodForMethod(_targetObject, methodList[i]);

if (newMethod)
[_replacementMethods setObject:newMethod forKey:methodList[i].name];
}

currentClass = currentClass.super_class;
}
}
Expand All @@ -153,7 +156,7 @@ var KVOProxyMap = [CPDictionary dictionary];
{
var proxy = [_CPKVOProxy proxyForObject:self],
imp = class_getInstanceMethod(proxy._nativeClass, aSelector);

return imp ? YES : NO;
}

Expand All @@ -166,10 +169,10 @@ var KVOProxyMap = [CPDictionary dictionary];
- (IMP)methodForSelector:(SEL)aSelector
{
var proxy = [_CPKVOProxy proxyForObject:self],
imp = class_getInstanceMethod(proxy._nativeClass, aSelector),
replacement = [proxy._replacementMethods objectForKey:aSelector];
replacement = [proxy._replacementMethods objectForKey:aSelector],
imp = replacement ? replacement : class_getInstanceMethod(proxy._nativeClass, aSelector);

return replacement ? replacement : imp;
return imp;
}

- (void)forwardInvocation:(CPInvocation)anInvocation
Expand All @@ -180,62 +183,66 @@ var KVOProxyMap = [CPDictionary dictionary];
if (method)
method.apply(self, anInvocation._arguments); //FIXME
else
[super forwardInvocation:anInvocation];
class_getInstanceMethod(proxy._nativeClass, @selector(forwardInvocation:)).apply(self, anInvocation.arguments);
}

- (void)_addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
{
if (!anObserver)
return;

var forwarder = nil;

if (aPath.indexOf('.') != CPNotFound)
return CPLog.error("WHOA, don't go crazy...");
else
forwarder = [[_CPKVOForwardingObserver alloc] initWithKeyPath:aPath object:_targetObject observer:anObserver options:options context:aContext];

var observers = [_observersForKey objectForKey:aPath];

if (!observers)
{
var observers = [_observersForKey objectForKey:aPath];

if (!observers)
{
observers = [CPDictionary dictionary];
[_observersForKey setObject:observers forKey:aPath];
}

[observers setObject:_CPKVOInfoMake(anObserver, options, aContext) forKey:[anObserver hash]];
observers = [CPDictionary dictionary];
[_observersForKey setObject:observers forKey:aPath];
}

var changes = [CPDictionary dictionary];
[changes setObject:[_targetObject valueForKey:aPath] forKey:CPKeyValueChangeNewKey];
[observers setObject:_CPKVOInfoMake(anObserver, options, aContext, forwarder) forKey:[anObserver hash]];

if (options & CPKeyValueObservingOptionInitial)
{
var newValue = [_targetObject valueForKeyPath:aPath];

[_changesForKey setObject:changes forKey:aPath];
if (!newValue && newValue !== "")
newValue = [CPNull null];

if (options & CPKeyValueObservingOptionInitial)
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
var changes = [CPDictionary dictionaryWithObject:newValue forKey:CPKeyValueChangeNewKey];
[anObserver observeValueForKeyPath:aPath ofObject:self change:changes context:aContext];
}
}

- (void)_removeObserver:(id)anObserver forKeyPath:(CPString)aPath
{
{
var observers = [_observersForKey objectForKey:aPath];

if (aPath.indexOf('.') != CPNotFound)
return CPLog.error("WHOA, don't go crazy...");
else
{
var observers = [_observersForKey objectForKey:aPath];
var forwarder = [observers objectForKey:[anObserver hash]].forwarder;
[forwarder finalize];
}

[observers removeObjectForKey:[anObserver hash]];

if (![observers count])
[_observersForKey removeObjectForKey:aPath];

if (![_observersForKey count])
{
_targetObject.isa = _nativeClass; //restore the original class
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
}
[observers removeObjectForKey:[anObserver hash]];

if (![observers count])
[_observersForKey removeObjectForKey:aPath];

if (![_observersForKey count])
{
_targetObject.isa = _nativeClass; //restore the original class
[KVOProxyMap removeObjectForKey:[_targetObject hash]];
}
}

- (void)willChangeValueForKey:(CPString)aKey
{
if (!aKey)
if (!aKey)
return;

[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:YES];
Expand All @@ -244,39 +251,36 @@ var KVOProxyMap = [CPDictionary dictionary];

- (void)didChangeValueForKey:(CPString)aKey
{
if (!aKey)
if (!aKey)
return;

[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:NO];
}

- (void)_sendNotificationsForKey:(CPString)aKey isBefore:(BOOL)isBefore
{
var changes = [_changesForKey objectForKey:aKey],
oldValue = [changes objectForKey:CPKeyValueChangeOldKey],
newValue = [changes objectForKey:CPKeyValueChangeNewKey];

if (!oldValue && oldValue !== "")
oldValue = [CPNull null];
var changes = [_changesForKey objectForKey:aKey];

if (!newValue && newValue !== "")
newValue = [CPNull null];

if (isBefore)
{
changes = [CPDictionary dictionary];


var oldValue = [_targetObject valueForKey:aKey];

if (!oldValue && oldValue !== "")
oldValue = [CPNull null];

[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
[changes setObject:newValue forKey:CPKeyValueChangeOldKey];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];

[_changesForKey setObject:changes forKey:aKey];
}
else
{
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];

var newValue = [_targetObject valueForKey:aKey];

if (!newValue && newValue !== "")
newValue = [CPNull null];

Expand All @@ -285,26 +289,121 @@ var KVOProxyMap = [CPDictionary dictionary];

var observers = [[_observersForKey objectForKey:aKey] allValues],
count = [observers count];

while (count--)
{
var observerInfo = observers[count];

if (isBefore && (observerInfo.options & CPKeyValueObservingOptionPrior))
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context];
else if (!isBefore)
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context];
}
}

- (BOOL)isKindOfClass:(Class)aClass
{
return [[self class] isSubclassOfClass:aClass];
}

- (BOOL)isMemberOfClass:(Class)aClass
{
return [self class] == aClass;
}

- (CPString)className
{
return [self class].name;
}

@end

@implementation _CPKVOForwardingObserver : CPObject
{
id _object;
id _observer;
id _context;
//a.b
CPString _firstPart; //a
CPString _secondPart; //b

id _value;
}

- (id)initWithKeyPath:(CPString)aKeyPath object:(id)anObject observer:(id)anObserver options:(unsigned)options context:(id)aContext
{
self = [super init];

_context = aContext;
_observer = anObserver;
_object = anObject;

//current ignoring options (FIXME?)

var dotIndex = aKeyPath.indexOf('.');

if (dotIndex == CPNotFound)
[CPException raise:CPInvalidArgumentException reason:"Created _CPKVOForwardingObserver without compound key path: "+aKeyPath];

_firstPart = aKeyPath.substring(0, dotIndex);
_secondPart = aKeyPath.substring(dotIndex+1);

//become an observer of the first part of our key (a)
[_object addObserver:self forKeyPath:_firstPart options:kvoNewAndOld context:nil];

//the current value of a (not the value of a.b)
_value = [_object valueForKey:_firstPart];

if (_value)
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil]; //we're observing b on current a

return self;
}

- (void)observeValueForKeyPath:(CPString)aKeyPath ofObject:(id)anObject change:(CPDictionary)changes context:(id)aContext
{
if (anObject == _object)
{
[_observer observeValueForKeyPath:_firstPart ofObject:_object change:changes context:_context];

//since a has changed, we should remove ourselves as an observer of the old a, and observe the new one
if (_value)
[_value removeObserver:self forKeyPath:_secondPart];

_value = [_object valueForKey:_firstPart];

if (_value)
[_value addObserver:self forKeyPath:_secondPart options:kvoNewAndOld context:nil];
}
else /* if (anObject == _value || !_value) */
{
//a is the same, but a.b has changed -- nothing to do but forward this message along
[_observer observeValueForKeyPath:_firstPart+"."+aKeyPath ofObject:_object change:changes context:_context];
}
}

- (void)finalize
{
if (_value)
[_value removeObserver:self forKeyPath:_secondPart];

[_object removeObserver:self forKeyPath:_firstPart];

_object = nil;
_observer = nil;
_context = nil;
_value = nil;
}

@end

var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext)
var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, aForwarder)
{
return {
observer: anObserver,
options: theOptions,
context: aContext
context: aContext,
forwarder: aForwarder
};
}

Expand Down

0 comments on commit 3834751

Please sign in to comment.