Add KVC+KVO support for "To Many" keys (i.e. Arrays).

This commit is contained in:
Ross Boucher committed 2009-01-20 18:51:10 -08:00
1 parent 490ad97f7b
commit 0b05b4b7d8
4 files changed
+648 -142

No files matched your search

+439
View File
@@ -0,0 +1,439 @@
/*
* CPArray+KVO.j
* Foundation
*
* Created by Ross Boucher.
* Copyright 2008, 280 North, Inc.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
@import "CPArray.j"
@implementation CPObject (CPArrayKVO)
- (id)mutableArrayValueForKey:(id)aKey
{
return [[_CPKVCArray alloc] initWithKey:aKey forProxyObject:self];
}
- (id)mutableArrayValueForKeyPath:(id)aKeyPath
{
var dotIndex = aKeyPath.indexOf(".");
if (dotIndex < 0)
return [self mutableArrayValueForKey:aKeyPath];
var firstPart = aKeyPath.substring(0, dotIndex),
lastPart = aKeyPath.substring(dotIndex+1);
return [[self valueForKeyPath:firstPart] valueForKeyPath:lastPart];
}
@end
@implementation _CPKVCArray : CPArray
{
id _proxyObject;
id _key;
SEL _insertSEL;
Function _insert;
SEL _removeSEL;
Function _remove;
SEL _replaceSEL;
Function _replace;
SEL _objectAtIndexSEL;
Function _objectAtIndex;
SEL _countSEL;
Function _count;
SEL _accessSEL;
Function _access;
SEL _setSEL;
Function _set;
}
+ (id)alloc
{
var a = [];
a.isa = self;
var ivars = class_copyIvarList(self),
count = ivars.length;
while (count--)
a[ivar_getName(ivars[count])] = nil;
return a;
}
-(id)initWithKey:(id)aKey forProxyObject:(id)anObject
{
self = [super init];
_key = aKey;
_proxyObject = anObject;
var capitalizedKey = _key.charAt(0).toUpperCase() + _key.substring(1);
_insertSEL = sel_getName(@"insertObject:in"+capitalizedKey+"AtIndex:");
if ([_proxyObject respondsToSelector:selector])
_insert = [_proxyObject methodForSelector:selector];
_removeSEL = sel_getName(@"removeObjectFrom"+capitalizedKey+"AtIndex:");
if ([_proxyObject respondsToSelector:selector])
_remove = [_proxyObject methodForSelector:selector];
_replaceSEL = sel_getName(@"replaceObjectFrom"+capitalizedKey+"AtIndex:withObject:");
if ([_proxyObject respondsToSelector:selector])
_replace = [_proxyObject methodForSelector:selector];
_objectAtIndexSEL = sel_getName(@"objectIn"+capitalizedKey+"AtIndex:");
if ([_proxyObject respondsToSelector:selector])
_objectAtIndex = [_proxyObject methodForSelector:selector];
_countSEL = sel_getName(@"countOf"+capitalizedKey);
if ([_proxyObject respondsToSelector:selector])
_count = [_proxyObject methodForSelector:selector];
_accessSEL = sel_getName(_key);
if ([_proxyObject respondsToSelector:selector])
_access = [_proxyObject methodForSelector:selector];
_setSEL = sel_getName(@"set"+capitalizedKey+":");
if ([_proxyObject respondsToSelector:selector])
_set = [_proxyObject methodForSelector:selector];
return self;
}
- (id)copy
{
var theCopy = [],
count = [self count];
for (var i=0; i<count; i++)
[theCopy addObject:[self objectAtIndex:i]];
return theCopy;
}
-(id)_representedObject
{
if (_access)
return _access(_proxyObject, _accessSEL);
return [_proxyObject valueForKey:_key];
}
-(void)_setRepresentedObject:(id)anObject
{
if (_set)
return _set(_proxyObject, _setSEL, anObject);
[_proxyObject setValue:anObject forKey:_key];
}
- (unsigned)count
{
if (_count)
return _count(_proxyObject, _countSEL);
return [[self _representedObject] count];
}
- (id)objectAtIndex:(unsigned)anIndex
{
if(_objectAtIndex)
return _objectAtIndex(_proxyObject, _objectAtIndexSEL, anIndex);
return [[self _representedObject] objectAtIndex:anIndex];
}
- (void)addObject:(id)anObject
{
if (_insert)
return _insert(_proxyObject, _insertSEL, anObject, [self count]);
var target = [[self _representedObject] copy];
[target addObject:anObject];
[_self _setRepresentedObject:target];
}
- (void)insertObject:(id)anObject atIndex:(unsigned)anIndex
{
if (_insert)
return _insert(_proxyObject, _insertSEL, anObject, anIndex);
var target = [[self _representedObject] copy];
[target insertObject:anObject atIndex:anIndex];
[_self _setRepresentedObject:target];
}
- (void)removeLastObject
{
if(_remove)
return _remove(_proxyObject, _removeSEL, [self count]-1);
var target = [[self _representedObject] copy];
[target removeLastObject];
[_self _setRepresentedObject:target];
}
- (void)removeObjectAtIndex:(unsigned)anIndex
{
if(_remove)
return _remove(_proxyObject, _removeSEL, anIndex);
var target = [[self _representedObject] copy];
[target removeObjectAtIndex:anIndex];
[_self _setRepresentedObject:target];
}
- (void)replaceObjectAtIndex:(unsigned)anIndex withObject:(id)anObject
{
if(_replace)
return _replace(_proxyObject, _replaceSEL, anIndex, anObject);
var target = [[self _representedObject] copy];
[target replaceObjectAtIndex:anIndex withObject:anObject];
[_self _setRepresentedObject:target];
}
- (CPArray)objectsAtIndexes:(CPIndexSet)indexes
{
var index = [indexes firstIndex],
objects = [];
while(index != CPNotFound)
{
[objects addObject:[self objectAtIndex:index]];
index = [indexes indexGreaterThanIndex:index];
}
return objects;
}
@end
//KVC on CPArray objects act on each item of the array, rather than on the array itself
@implementation CPArray (KeyValueCoding)
- (id)valueForKey:(CPString)aKey
{
if (aKey.indexOf("@") === 0)
{
if (aKey.indexOf(".") !== -1)
[CPException raise:CPInvalidArgumentException reason:"called valueForKey: on an array with a complex key ("+aKey+"). use valueForKeyPath:"];
if (aKey == "@count")
return length;
return nil;
}
else
{
var newArray = [],
enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
{
var value = [object valueForKey:aKey];
if (!value && value !== "")
value = [NSNull null];
newArray.push(value);
}
return newArray;
}
}
- (id)valueForKeyPath:(CPString)aKeyPath
{
if (aKey.indexOf("@") === 0)
{
var dotIndex = akey.indexOf("."),
operator = aKey.substring(1, dotIndex),
parameter = aKey.substring(dotIndex+1);
if (kvoOperators[operator])
return kvoOperators[operator](self, _cmd, parameter);
return nil;
}
else
{
var newArray = [],
enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
{
var value = [object valueForKeyPath:aKeyPath];
if (!value && value !== "")
value = [NSNull null];
newArray.push(value);
}
return newArray;
}
}
- (void)setValue:(id)aValue forKey:(CPString)aKey
{
var enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
[object setValue:aValue forKey:aKey];
}
- (void)setValue:(id)aValue forKeyPath:(CPString)aKeyPath
{
var enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
[object setValue:aValue forKeyPath:aKeyPath];
}
@end
var kvoOperators = [];
kvoOperators["avg"] = function avgOperator(self, _cmd, param)
{
var objects = [self valueForKeyPath:param],
length = [objects count],
average = 0.0;
while(length--)
average += [objects[length] doubleValue] / count;
return average;
}
kvoOperators["max"] = function maxOperator(self, _cmd, param)
{
var objects = [self valueForKeyPath:param],
length = [objects count] - 1,
max = [objects lastObject];
while (length--)
{
var item = objects[length];
if ([max compare:item] < 0)
max = item;
}
return max;
}
kvoOperators["min"] = function minOperator(self, _cmd, param)
{
var objects = [self valueForKeyPath:param],
length = [objects count] - 1,
min = [objects lastObject];
while (length--)
{
var item = objects[length];
if ([min compare:item] > 0)
min = item;
}
return min;
}
kvoOperators["count"] = function countOperator(self, _cmd, param)
{
return [self count];
}
kvoOperators["sum"] = function sumOperator(self, _cmd, param)
{
var objects = [self valueForKeyPath:param],
length = [objects count],
sum = 0.0;
while(length--)
sum += [objects[length] doubleValue];
return sum;
}
@implementation CPArray (KeyValueObserving)
- (void)addObserver:(id)anObserver toObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
{
var index = [indexes firstIndex];
while (index >= 0)
{
[self[index] addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
index = [indexes indexGreaterThanIndex:index];
}
}
- (void)removeObserver:(id)anObserver fromObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath
{
var index = [indexes firstIndex];
while (index >= 0)
{
[self[index] removeObserver:anObserver forKeyPath:aKeyPath];
index = [indexes indexGreaterThanIndex:index];
}
}
-(void)addObserver:(id)observer forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
{
if ([isa instanceMethodForSelector:_cmd]==[NSArray instanceMethodForSelector:_cmd])
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
else
[super addObserver:observer forKeyPath:aKeyPath options:options context:context];
}
-(void)removeObserver:(id)observer forKeyPath:(CPString)aKeyPath
{
if ([isa instanceMethodForSelector:_cmd]==[NSArray instanceMethodForSelector:_cmd])
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
else
[super removeObserver:observer forKeyPath:aKeyPath];
}
@end
+1 -78
View File
@@ -278,81 +278,4 @@ CPUnknownUserInfoKey = @"CPUnknownUserInfoKey";
@end
//KVC on CPArray objects act on each item of the array, rather than on the array itself
@implementation CPArray (KeyValueCoding)
- (id)valueForKey:(CPString)aKey
{
if (aKey.indexOf("@") == 0)
{
if (aKey == "@count")
return length;
return nil;
}
else
{
var newArray = [],
enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
{
var value = [object valueForKey:aKey];
if (!value && value !== "")
value = [NSNull null];
newArray.push(value);
}
return newArray;
}
}
- (id)valueForKeyPath:(CPString)aKeyPath
{
if (aKey.indexOf("@") == 0)
{
return nil;
}
else
{
var newArray = [],
enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
{
var value = [object valueForKeyPath:aKeyPath];
if (!value && value !== "")
value = [NSNull null];
newArray.push(value);
}
return newArray;
}
}
- (void)setValue:(id)aValue forKey:(CPString)aKey
{
var enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
[object setValue:aValue forKey:aKey];
}
- (void)setValue:(id)aValue forKeyPath:(CPString)aKeyPath
{
var enumerator = [self objectEnumerator],
object;
while (object = [enumerator nextObject])
[object setValue:aValue forKeyPath:aKeyPath];
}
@end
@import "CPArray_KVO.j"
+118 -54
View File
@@ -31,12 +31,18 @@
- (void)willChangeValueForKey:(CPString)aKey
{
}
- (void)didChangeValueForKey:(CPString)aKey
{
}
- (void)willChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)key
{
}
- (void)didChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)key
{
}
- (void)addObserver:(id)anObserver forKeyPath:(CPString)aPath options:(unsigned)options context:(id)aContext
@@ -79,9 +85,6 @@ CPKeyValueObservingOptionOld = 1 << 1;
CPKeyValueObservingOptionInitial = 1 << 2;
CPKeyValueObservingOptionPrior = 1 << 3;
//convenience
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
// KVO Change Dictionary Keys
CPKeyValueChangeKindKey = @"CPKeyValueChangeKindKey";
CPKeyValueChangeNewKey = @"CPKeyValueChangeNewKey";
@@ -89,6 +92,15 @@ CPKeyValueChangeOldKey = @"CPKeyValueChangeOldKey";
CPKeyValueChangeIndexesKey = @"CPKeyValueChangeIndexesKey";
CPKeyValueChangeNotificationIsPriorKey = @"CPKeyValueChangeNotificationIsPriorKey";
// KVO Change Types
CPKeyValueChangeSetting = 1;
CPKeyValueChangeInsertion = 2;
CPKeyValueChangeRemoval = 3;
CPKeyValueChangeReplacement = 4;
//convenience
var kvoNewAndOld = CPKeyValueObservingOptionNew|CPKeyValueObservingOptionOld;
// Map of real objects to their KVO proxy
var KVOProxyMap = [CPDictionary dictionary],
DependentKeysMap = [CPDictionary dictionary];
@@ -173,22 +185,23 @@ var KVOProxyMap = [CPDictionary dictionary],
capitalizedKey = aKey.charAt(0).toUpperCase() + aKey.substring(1),
found = false,
replacementMethods = [
"set"+capitalizedKey+":",
"_set"+capitalizedKey+":",
"insertObject:in"+capitalizedKey+"AtIndex:",
"replaceObjectIn"+capitalizedKey+"AtIndex:",
"removeObjectFrom"+capitalizedKey+"AtIndex:"
"set"+capitalizedKey+":", _kvoMethodForMethod,
"_set"+capitalizedKey+":", _kvoMethodForMethod,
"insertObject:in"+capitalizedKey+"AtIndex:", _kvoInsertMethodForMethod,
"replaceObjectIn"+capitalizedKey+"AtIndex:withObject:", _kvoReplaceMethodForMethod,
"removeObjectFrom"+capitalizedKey+"AtIndex:", _kvoRemoveMethodForMethod
];
for (var i=0, count=replacementMethods.length; i<count; i++)
for (var i=0, count=replacementMethods.length; i<count; i+=2)
{
var theSelector = sel_getName(replacementMethods[i]);
var theSelector = sel_getName(replacementMethods[i]),
theReplacementMethod = replacementMethods[i+1];
if ([_nativeClass instancesRespondToSelector:theSelector])
{
var theMethod = class_getInstanceMethod(_nativeClass, theSelector);
class_addMethod(_targetObject.isa, theSelector, _kvoMethodForMethod(aKey, theMethod), "");
class_addMethod(_targetObject.isa, theSelector, theReplacementMethod(aKey, theMethod), "");
found = true;
}
@@ -282,21 +295,41 @@ var KVOProxyMap = [CPDictionary dictionary],
}
}
- (void)_sendNotificationsForKey:(CPString)aKey isBefore:(BOOL)isBefore
//FIXME: We do not compute and cache if CPKeyValueObservingOptionOld is needed, so we may do unnecessary work
- (void)_sendNotificationsForKey:(CPString)aKey changeOptions:(CPDictionary)changeOptions isBefore:(BOOL)isBefore
{
var changes = [_changesForKey objectForKey:aKey];
if (isBefore)
{
changes = [CPDictionary dictionary];
changes = changeOptions;
var oldValue = [_targetObject valueForKey:aKey];
if (!oldValue && oldValue !== "")
oldValue = [CPNull null];
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
if (indexes)
{
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for to-many relationships, oldvalue is only sensible for replace and remove
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeRemoval)
{
//FIXME: do we need to go through and replace "" with CPNull?
var oldValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:oldValues forKey:CPKeyValueChangeOldKey];
}
}
else
{
var oldValue = [_targetObject valueForKey:aKey];
if (!oldValue && oldValue !== "")
oldValue = [CPNull null];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
}
[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
[_changesForKey setObject:changes forKey:aKey];
}
@@ -304,12 +337,29 @@ var KVOProxyMap = [CPDictionary dictionary],
{
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
var newValue = [_targetObject valueForKey:aKey];
if (!newValue && newValue !== "")
newValue = [CPNull null];
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey];
if (indexes)
{
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for to-many relationships, oldvalue is only sensible for replace and remove
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeInsertion)
{
//FIXME: do we need to go through and replace "" with CPNull?
var oldValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:oldValues forKey:CPKeyValueChangeNewKey];
}
}
else
{
var newValue = [_targetObject valueForKey:aKey];
if (!newValue && newValue !== "")
newValue = [CPNull null];
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
}
}
var observers = [[_observersForKey objectForKey:aKey] allValues],
@@ -331,7 +381,7 @@ var KVOProxyMap = [CPDictionary dictionary],
return;
for (var i=0, count=keysComposedOfKey.length; i<count; i++)
[self _sendNotificationsForKey:keysComposedOfKey[i] isBefore:isBefore];
[self _sendNotificationsForKey:keysComposedOfKey[i] changeOptions:changeOptions isBefore:isBefore];
}
@end
@@ -345,7 +395,9 @@ var KVOProxyMap = [CPDictionary dictionary],
if (!aKey)
return;
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:YES];
var changeOptions = [CPDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES];
}
- (void)didChangeValueForKey:(CPString)aKey
@@ -353,7 +405,25 @@ var KVOProxyMap = [CPDictionary dictionary],
if (!aKey)
return;
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey isBefore:NO];
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO];
}
- (void)willChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
{
if (!aKey)
return;
var changeOptions = [CPDictionary dictionaryWithObjects:[change, indexes] forKeys:[CPKeyValueChangeKindKey, CPKeyValueChangeIndexesKey]];
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:changeOptions isBefore:YES];
}
- (void)didChange:(CPKeyValueChange)change valuesAtIndexes:(CPIndexSet)indexes forKey:(CPString)aKey
{
if (!aKey)
return;
[[_CPKVOProxy proxyForObject:self] _sendNotificationsForKey:aKey changeOptions:nil isBefore:NO];
}
- (Class)class
@@ -474,48 +544,42 @@ var _CPKVOInfoMake = function _CPKVOInfoMake(anObserver, theOptions, aContext, a
var _kvoMethodForMethod = function _kvoMethodForMethod(theKey, theMethod)
{
return function(self)
return function(self, _cmd, object)
{
[self willChangeValueForKey:theKey];
theMethod.method_imp.apply(self, arguments);
theMethod.method_imp(self, _cmd, object);
[self didChangeValueForKey:theKey];
}
}
@implementation CPArray (KeyValueObserving)
- (void)addObserver:(id)anObserver toObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
var _kvoInsertMethodForMethod = function _kvoInsertMethodForMethod(theKey, theMethod)
{
var index = [indexes firstIndex];
while (index >= 0)
return function(self, _cmd, object, index)
{
[self[index] addObserver:anObserver forKeyPath:aKeyPath options:options context:context];
index = [indexes indexGreaterThanIndex:index];
[self willChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
theMethod.method_imp(self, _cmd, object, index);
[self didChange:CPKeyValueChangeInsertion valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
}
}
- (void)removeObserver:(id)anObserver fromObjectsAtIndexes:(CPIndexSet)indexes forKeyPath:(CPString)aKeyPath
var _kvoReplaceMethodForMethod = function _kvoReplaceMethodForMethod(theKey, theMethod)
{
var index = [indexes firstIndex];
while (index >= 0)
return function(self, _cmd, index, object)
{
[self[index] removeObserver:anObserver forKeyPath:aKeyPath];
index = [indexes indexGreaterThanIndex:index];
[self willChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
theMethod.method_imp(self, _cmd, index, object);
[self didChange:CPKeyValueChangeReplacement valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
}
}
-(void)addObserver:(id)observer forKeyPath:(CPString)aKeyPath options:(unsigned)options context:(id)context
var _kvoRemoveMethodForMethod = function _kvoRemoveMethodForMethod(theKey, theMethod)
{
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
return function(self, _cmd, index)
{
[self willChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey];
theMethod.method_imp(self, _cmd, index);
[self didChange:CPKeyValueChangeRemoval valuesAtIndexes:[CPIndexSet indexSetWithIndex:index] forKey:theKey]
}
}
-(void)removeObserver:(id)observer forKeyPath:(CPString)aKeyPath
{
[CPException raise:CPInvalidArgumentException reason:"Unsupported method on CPArray"];
}
@end
@import "CPArray_KVO.j"
+90 -10
View File
@@ -1,5 +1,5 @@
import <Foundation/CPKeyValueCoding.j>
import <Foundation/CPKeyValueObserving.j>
@import <Foundation/CPKeyValueCoding.j>
@import <Foundation/CPKeyValueObserving.j>
@implementation CPKVOTest : OJTestCase
{
@@ -14,6 +14,11 @@ import <Foundation/CPKeyValueObserving.j>
id focus;
}
- (void)setUp
{
_sawObservation = NO;
}
- (void)testAddObserver
{
bob = [[PersonTester alloc] init];
@@ -167,7 +172,7 @@ import <Foundation/CPKeyValueObserving.j>
[cs101 setTeacher:bob];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testMultipartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:0 context:"testMultipartKey"];
[bob setName:@"bob"];
@@ -184,7 +189,7 @@ import <Foundation/CPKeyValueObserving.j>
[cs101 setTeacher:bob];
[bob setValue:focus forKey:"car"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testThreePartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:0 context:"testThreePartKey"];
[focus setValue:"ford focus" forKey:"model"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
@@ -199,7 +204,7 @@ import <Foundation/CPKeyValueObserving.j>
[cs101 setTeacher:bob];
[focus setValue:"2000" forKey:"year"];
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:nil context:"testThreePartKeyPart2"];
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:0 context:"testThreePartKeyPart2"];
[bob setValue:focus forKey:"car"];
@@ -213,7 +218,7 @@ import <Foundation/CPKeyValueObserving.j>
[cs101 setTeacher:bob];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:nil context:"testRemoveMultipartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:0 context:"testRemoveMultipartKey"];
[cs101 removeObserver:self forKeyPath:@"teacher.name"];
@@ -231,7 +236,7 @@ import <Foundation/CPKeyValueObserving.j>
[cs101 setTeacher:bob];
[bob setValue:focus forKey:"car"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:nil context:"testRemoveThreePartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:0 context:"testRemoveThreePartKey"];
[cs101 removeObserver:self forKeyPath:@"teacher.car.model"];
[focus setValue:"ford focus" forKey:"model"];
@@ -250,7 +255,7 @@ import <Foundation/CPKeyValueObserving.j>
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges"];
[a setValue:[D new] forKeyPath:"b.c.d"];
@@ -267,7 +272,7 @@ import <Foundation/CPKeyValueObserving.j>
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges2"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges2"];
[a setValue:nil forKeyPath:"b.c"];
@@ -284,13 +289,42 @@ import <Foundation/CPKeyValueObserving.j>
[a setValue:[E new] forKeyPath:"b.c.d.e"];
[a setValue:[F new] forKeyPath:"b.c.d.e.f"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:nil context:"testCrazyKeyPathChanges3"];
[a addObserver:self forKeyPath:"b.c.d.e.f" options:0 context:"testCrazyKeyPathChanges3"];
[a setValue:7 forKeyPath:"b.c.d.e.f"];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testInsertIntoToManyProperty
{
var tester = [ToManyTester new];
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:"testInsertIntoToManyProperty"];
[tester insertObject:5 inManagedObjectsAtIndex:4];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testRemoveFromToManyProperty
{
var tester = [ToManyTester new];
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:"testRemoveFromToManyProperty"];
[tester removeObjectFromManagedObjectsAtIndex:0];
[self assertTrue: _sawObservation message:"Never recieved an observation"];
}
- (void)testPerformance
{
bob = [PersonTester new];
@@ -432,6 +466,25 @@ import <Foundation/CPKeyValueObserving.j>
_sawDependentObservation = YES;
break;
case "testInsertIntoToManyProperty":
var type = [changes objectForKey:CPKeyValueChangeKindKey];
[self assertTrue: type == CPKeyValueChangeInsertion message: "Should have been an insertion, was: "+type];
var values = [changes objectForKey:CPKeyValueChangeNewKey];
[self assertTrue: [values isEqual:[5]] message: "array should have contained 5, was: "+values+" type: "+[values.isa description]+" length: "+values.length];
break;
case "testRemoveFromToManyProperty":
var type = [changes objectForKey:CPKeyValueChangeKindKey];
[self assertTrue: type == CPKeyValueChangeRemoval message: "Should have been a removal, was: "+type];
var values = [changes objectForKey:CPKeyValueChangeOldKey];
[self assertTrue: [values isEqual:[1]] message: "array should have contained 1, was: "+values+" type: "+[values.isa description]+" length: "+values.length];
[[anObject valueForKey:@"managedObjects"] isEqual:[2, 3, 4]];
break;
default:
[self assertFalse:YES message:"unhandled observation, must be an error"];
return;
@@ -501,6 +554,33 @@ import <Foundation/CPKeyValueObserving.j>
@end
@implementation ToManyTester : CPObject
{
CPArray managedObjects;
}
- (unsigned int)countOfManagedObjects
{
return [managedObjects count];
}
- (id)objectInManagedObjectsAtIndex:(unsigned)anIndex
{
return [managedObjects objectAtIndex:anIndex];
}
- (void)removeObjectFromManagedObjectsAtIndex:(unsigned)anIndex
{
[managedObjects removeObjectAtIndex:anIndex];
}
- (void)insertObject:(id)anObject inManagedObjectsAtIndex:(unsigned)anIndex
{
[managedObjects insertObject:anObject atIndex:anIndex];
}
@end
@implementation A : CPObject
{
id b;