Merge pull request #2224 from mrcarlberg/kvo_honor_options_when_creating_change_dictionary

KVO now honor options when creating change dictionary
This commit is contained in:
Antoine Mercadal committed 2014-10-20 18:42:09 -07:00
commit f2956670cd
5 files changed
+583 -129

No files matched your search

+1 -1
View File
@@ -156,7 +156,7 @@ var CPBindingOperationAnd = 0,
[self _updatePlaceholdersWithOptions:options forBinding:aName];
[aDestination addObserver:self forKeyPath:aKeyPath options:CPKeyValueObservingOptionNew context:aBinding];
[aDestination addObserver:self forKeyPath:aKeyPath options:0 context:aBinding];
var bindings = [bindingsMap objectForKey:[_source UID]];
+172 -93
View File
@@ -327,6 +327,7 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
Class _nativeClass;
CPDictionary _changesForKey;
CPDictionary _nestingForKey;
CPDictionary _minOptionsForKey;
Object _observersForKey;
int _observersForKeyLength;
CPSet _replacedKeys;
@@ -354,6 +355,7 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
_observersForKey = {};
_changesForKey = {};
_nestingForKey = {};
_minOptionsForKey = {};
_observersForKeyLength = 0;
[self _replaceClass];
@@ -767,12 +769,20 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
if (options & CPKeyValueObservingOptionInitial)
{
var newValue = [_targetObject valueForKeyPath:aPath];
var changes;
if (newValue === nil || newValue === undefined)
newValue = [CPNull null];
if (options & CPKeyValueObservingOptionNew)
{
var newValue = [_targetObject valueForKeyPath:aPath];
if (newValue == nil)
newValue = [CPNull null];
changes = @{ CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNewKey: newValue };
} else {
changes = @{ CPKeyValueChangeKindKey: CPKeyValueChangeSetting };
}
var changes = @{ CPKeyValueChangeNewKey: newValue };
[anObserver observeValueForKeyPath:aPath ofObject:_targetObject change:changes context:aContext];
}
}
@@ -819,7 +829,9 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
- (void)_sendNotificationsForKey:(CPString)aKey changeOptions:(CPDictionary)changeOptions isBefore:(BOOL)isBefore
{
var changes = _changesForKey[aKey];
var changes = _changesForKey[aKey],
observers = [_observersForKey[aKey] allValues],
observersMinimumOptions = 0;
if (isBefore)
{
@@ -838,61 +850,78 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
_nestingForKey[aKey] = 1;
// Get the combined minimum of the ...Old and ...New options for all observers
var count = observers ? observers.length : 0;
while (count--)
{
var observerInfo = observers[count];
observersMinimumOptions |= observerInfo.options & kvoNewAndOld;
}
_minOptionsForKey[aKey] = observersMinimumOptions;
changes = changeOptions;
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey],
setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey];
if (setMutationKind)
if (observersMinimumOptions & CPKeyValueObservingOptionOld)
{
var setMutationObjects = [changes[_CPKeyValueChangeSetMutationObjectsKey] copy],
setExistingObjects = [[_targetObject valueForKey: aKey] copy];
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey],
setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey];
if (setMutationKind == CPKeyValueMinusSetMutation)
if (setMutationKind)
{
[setExistingObjects intersectSet: setMutationObjects];
[changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey];
var setMutationObjects = [changes[_CPKeyValueChangeSetMutationObjectsKey] copy],
setExistingObjects = [[_targetObject valueForKey: aKey] copy];
if (setMutationKind == CPKeyValueMinusSetMutation)
{
[setExistingObjects intersectSet: setMutationObjects];
[changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey];
}
else if (setMutationKind === CPKeyValueIntersectSetMutation || setMutationKind === CPKeyValueSetSetMutation)
{
[setExistingObjects minusSet: setMutationObjects];
[changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey];
}
//for unordered to-many relationships (CPSet) even new values can only be calculated before!!!
if (setMutationKind === CPKeyValueUnionSetMutation || setMutationKind === CPKeyValueSetSetMutation)
{
[setMutationObjects minusSet: setExistingObjects];
//hide new value (for CPKeyValueObservingOptionPrior messages)
//as long as "didChangeValue..." is not yet called!
changes[_CPKeyValueChangeSetMutationNewValueKey] = setMutationObjects;
}
}
else if (setMutationKind === CPKeyValueIntersectSetMutation || setMutationKind === CPKeyValueSetSetMutation)
else if (indexes)
{
[setExistingObjects minusSet: setMutationObjects];
[changes setValue:setExistingObjects forKey:CPKeyValueChangeOldKey];
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for ordered 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 newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:newValues forKey:CPKeyValueChangeOldKey];
}
}
else
{
var oldValue = [_targetObject valueForKey:aKey];
if (oldValue === nil || oldValue === undefined)
oldValue = [CPNull null];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
}
//for unordered to-many relationships (CPSet) even new values can only be calculated before!!!
if (setMutationKind === CPKeyValueUnionSetMutation || setMutationKind === CPKeyValueSetSetMutation)
{
[setMutationObjects minusSet: setExistingObjects];
//hide new value (for CPKeyValueObservingOptionPrior messages)
//as long as "didChangeValue..." is not yet called!
changes[_CPKeyValueChangeSetMutationNewValueKey] = setMutationObjects;
}
}
else if (indexes)
{
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for ordered 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 newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:newValues forKey:CPKeyValueChangeOldKey];
}
}
else
{
var oldValue = [_targetObject valueForKey:aKey];
if (oldValue === nil || oldValue === undefined)
oldValue = [CPNull null];
[changes setObject:oldValue forKey:CPKeyValueChangeOldKey];
}
[changes setObject:1 forKey:CPKeyValueChangeNotificationIsPriorKey];
_changesForKey[aKey] = changes;
// Clear ...New option as it should never be sent for a ...Prior option
observersMinimumOptions &= ~CPKeyValueObservingOptionNew;
}
else
{
@@ -927,55 +956,97 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
[changes removeObjectForKey:CPKeyValueChangeNotificationIsPriorKey];
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey],
setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey];
observersMinimumOptions = _minOptionsForKey[aKey];
if (setMutationKind)
if (observersMinimumOptions & CPKeyValueObservingOptionNew)
{
//old and new values for unordered to-many relationships can only be calculated before
//set recalculated hidden new value as soon as "didChangeValue..." is called!
var newValue = changes[_CPKeyValueChangeSetMutationNewValueKey];
[changes setValue:newValue forKey:CPKeyValueChangeNewKey];
var indexes = [changes objectForKey:CPKeyValueChangeIndexesKey],
setMutationKind = changes[_CPKeyValueChangeSetMutationKindKey];
//delete hidden values
delete changes[_CPKeyValueChangeSetMutationNewValueKey];
delete changes[_CPKeyValueChangeSetMutationObjectsKey];
delete changes[_CPKeyValueChangeSetMutationKindKey];
}
else if (indexes)
{
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for ordered to-many relationships, newvalue is only sensible for replace and insert
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeInsertion)
if (setMutationKind)
{
//FIXME: do we need to go through and replace "" with CPNull?
var newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:newValues forKey:CPKeyValueChangeNewKey];
//old and new values for unordered to-many relationships can only be calculated before
//set recalculated hidden new value as soon as "didChangeValue..." is called!
var newValue = changes[_CPKeyValueChangeSetMutationNewValueKey];
[changes setValue:newValue forKey:CPKeyValueChangeNewKey];
//delete hidden values
delete changes[_CPKeyValueChangeSetMutationNewValueKey];
delete changes[_CPKeyValueChangeSetMutationObjectsKey];
delete changes[_CPKeyValueChangeSetMutationKindKey];
}
else if (indexes)
{
var type = [changes objectForKey:CPKeyValueChangeKindKey];
// for ordered to-many relationships, newvalue is only sensible for replace and insert
if (type == CPKeyValueChangeReplacement || type == CPKeyValueChangeInsertion)
{
//FIXME: do we need to go through and replace "" with CPNull?
var newValues = [[_targetObject mutableArrayValueForKeyPath:aKey] objectsAtIndexes:indexes];
[changes setValue:newValues forKey:CPKeyValueChangeNewKey];
}
}
else
{
var newValue = [_targetObject valueForKey:aKey];
if (newValue === nil || newValue === undefined)
newValue = [CPNull null];
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
}
}
delete _minOptionsForKey[aKey];
delete _changesForKey[aKey];
}
var count = observers ? observers.length : 0,
changesCache = {};
while (count--)
{
var observerInfo = observers[count],
options = observerInfo.options,
onlyNewAndOldOptions = options & kvoNewAndOld,
observerChanges = nil;
if (isBefore)
{
// Only send 'observeValueForKeyPath:' for '...Prior' option when handling 'willChangeValue...'
if (options & CPKeyValueObservingOptionPrior)
{
observerChanges = changes;
// The new values are not yet created in the change dictionary so remove ...New option to get a working cache below
onlyNewAndOldOptions &= ~CPKeyValueObservingOptionNew;
}
}
else
{
var newValue = [_targetObject valueForKey:aKey];
if (newValue === nil || newValue === undefined)
newValue = [CPNull null];
[changes setObject:newValue forKey:CPKeyValueChangeNewKey];
observerChanges = changes;
}
delete _changesForKey[aKey];
}
var observers = [_observersForKey[aKey] allValues],
count = observers ? observers.length : 0;
while (count--)
{
var observerInfo = observers[count];
if (!isBefore || (observerInfo.options & CPKeyValueObservingOptionPrior))
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:changes context:observerInfo.context];
if (observerChanges)
{
// Don't change the 'change' dictionary when the observer wants the minimum options.
// The ...New option is remved above for the ...Prior case
if (onlyNewAndOldOptions !== observersMinimumOptions)
{
// Use a subset of the 'change' dictionary. First try to find it in the cache
observerChanges = changesCache[onlyNewAndOldOptions];
if (!observerChanges)
{
// Not in the cache. Build a new dictionary and store it in the cache
changesCache[onlyNewAndOldOptions] = observerChanges = [changes mutableCopy];
if (!(onlyNewAndOldOptions & CPKeyValueObservingOptionOld))
[observerChanges removeObjectForKey:CPKeyValueChangeOldKey];
if (!(onlyNewAndOldOptions & CPKeyValueObservingOptionNew))
[observerChanges removeObjectForKey:CPKeyValueChangeNewKey];
}
}
[observerInfo.observer observeValueForKeyPath:aKey ofObject:_targetObject change:observerChanges context:observerInfo.context];
}
}
var dependentKeysMap = _nativeClass[DependentKeysKey];
@@ -1226,13 +1297,21 @@ var kvoNewAndOld = CPKeyValueObservingOptionNew | CPKeyValueObservingOpti
{
if (aKeyPath === _firstPart)
{
var oldValue = [_value valueForKeyPath:_secondPart],
newValue = [_object valueForKeyPath:_firstPart + "." + _secondPart],
pathChanges = @{
CPKeyValueChangeNewKey: newValue ? newValue : [CPNull null],
CPKeyValueChangeOldKey: oldValue ? oldValue : [CPNull null],
CPKeyValueChangeKindKey: CPKeyValueChangeSetting,
};
var pathChanges = [CPMutableDictionary dictionaryWithObject:CPKeyValueChangeSetting forKey:CPKeyValueChangeKindKey];
if (_options & CPKeyValueObservingOptionOld)
{
var oldValue = [_value valueForKeyPath:_secondPart];
[pathChanges setObject:oldValue != null ? oldValue : [CPNull null] forKey:CPKeyValueChangeOldKey];
}
if (_options & CPKeyValueObservingOptionNew)
{
var newValue = [_object valueForKeyPath:_firstPart + "." + _secondPart];
[pathChanges setObject:newValue != null ? newValue : [CPNull null] forKey:CPKeyValueChangeNewKey];
}
[_observer observeValueForKeyPath:_firstPart + "." + _secondPart ofObject:_object change:pathChanges context:_context];
+85
View File
@@ -10,6 +10,9 @@
CPArray observations;
int aCount @accessors;
CPArray nameValues @accessors;
CPArray depValues @accessors;
}
- (CPArray)makeTestArray
@@ -19,6 +22,22 @@
[Employee employeeWithName:@"Tom" department:[Department departmentWithName:@"CommonJS"]]];
}
- (CPArray)makeTestArrayMany
{
var a = [[Employee employeeWithName:@"Francisco" department:[Department departmentWithName:@"Cappuccino"]],
[Employee employeeWithName:@"Ross" department:[Department departmentWithName:@"Cappuccino"]],
[Employee employeeWithName:@"Tom" department:[Department departmentWithName:@"CommonJS"]]];
var i = 10000;
while (i--)
{
[a addObject:[Employee employeeWithName:@"Martin" + i department:[Department departmentWithName:@"Magic Lightning"]]];
}
return a;
}
- (void)initControllerWithSimpleArray
{
// Copy the array to allow the original to be reused.
@@ -1054,6 +1073,54 @@
[self assert:[1, 2, 3] equals:[arrayController1 arrangedObjects] message:"normal selection"];
}
/*!
Test that bindings are not generation any change dictionary with Old or New values as a observer
*/
- (void)testCreationOfOldAndNeChangeDictionaryinObserverProtocol
{
[self initControllerWithContentBinding];
var arrayController = [self arrayController];
[self bind:@"nameValues" toObject:arrayController withKeyPath:@"arrangedObjects.name" options:nil];
countGetName = 0;
[self setItemsArray:[]];
[self assert:0 equals:countGetName];
countGetName = 0;
[self setItemsArray:[self makeTestArray]];
[self assert:3 equals:countGetName message:@"3 items in array and one '.name' bindings should make 3 accesses to the name property not " + countGetName + " times"];
}
/*!
Test the speed of set an big array when the old was an empty.
Also test the speed when an empty array is set and the old is an big
*/
- (void)testPerformance
{
[self initControllerWithContentBinding];
var arrayController = [self arrayController];
[self bind:@"nameValues" toObject:arrayController withKeyPath:@"arrangedObjects.name" options:nil];
[self bind:@"depValues" toObject:arrayController withKeyPath:@"arrangedObjects.department" options:nil];
[self setItemsArray:[]];
var bigArray = [self makeTestArrayMany];
var d = new Date();
[self setItemsArray:bigArray];
var dd = new Date();
[self setItemsArray:[]];
var ddd = new Date();
CPLog.warn("\n" + [self className] + " Performance tests");
CPLog.warn("EmptyArray -> BigArray (" + [bigArray count] + "): " + (dd - d) + "ms.");
CPLog.warn("BigArray (" + [bigArray count] + ") -> EmptyArray: " + (ddd - dd) + "ms.");
}
- (void)observeValueForKeyPath:keyPath
ofObject:anActivity
change:change
@@ -1099,8 +1166,20 @@
[_contentArray replaceObjectAtIndex:index withObject:anObject];
}
- (void)setItemsArray:(CPArray)newValue
{
[self setValue:newValue forKey:@"contentArray"];
}
- (CPArray)itemsArray
{
return [self valueForKey:@"contentArray"];
}
@end
var countGetName;
@implementation Employee : CPObject
{
CPString _name @accessors(property=name);
@@ -1128,6 +1207,12 @@
return [CPString stringWithFormat:@"<Employee %@>", [self name]];
}
- (CPString)name
{
countGetName++;
return _name;
}
@end
@implementation Department : CPObject
+89 -31
View File
@@ -29,7 +29,7 @@
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:@"testAddObserver"];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testAddObserver"];
[bob setValue:@"bob" forKey:@"name"];
@@ -42,7 +42,7 @@
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:@"testUnobservedKey"];
[bob addObserver:self forKeyPath:@"name" options:0 context:@"testUnobservedKey"];
[bob setValue:@"555" forKey:@"phoneNumber"];
@@ -54,8 +54,8 @@
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:@"testAddTwoObservers"];
[bob addObserver:[CPObject new] forKeyPath:@"name" options:nil context:@"testAddTwoObservers"];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testAddTwoObservers"];
[bob addObserver:[CPObject new] forKeyPath:@"name" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testAddTwoObservers"];
[bob setValue:@"bob" forKey:@"name"];
@@ -68,7 +68,7 @@
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"phoneNumber" options:nil context:@"testDirectIVarObservation"];
[bob addObserver:self forKeyPath:@"phoneNumber" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testDirectIVarObservation"];
[bob setValue:@"555" forKey:@"phoneNumber"];
@@ -81,8 +81,8 @@
{
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:nil context:@"testRemoveObserver"];
[bob addObserver:[CPString new] forKeyPath:@"name" options:nil context:@"testRemoveObserver"];
[bob addObserver:self forKeyPath:@"name" options:0 context:@"testRemoveObserver"];
[bob addObserver:[CPString new] forKeyPath:@"name" options:0 context:@"testRemoveObserver"];
[bob removeObserver:self forKeyPath:@"name"];
@@ -96,8 +96,8 @@
bob = [[PersonTester alloc] init];
obj = [CPString new];
[bob addObserver:self forKeyPath:@"name" options:nil context:@"testRemoveOtherObserver"];
[bob addObserver:obj forKeyPath:@"name" options:nil context:@"testRemoveOtherObserver"];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testRemoveOtherObserver"];
[bob addObserver:obj forKeyPath:@"name" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testRemoveOtherObserver"];
[bob removeObserver:obj forKeyPath:@"name"];
@@ -113,8 +113,8 @@
obj = [CPArray new];
obj2 = [CPString new];
[bob addObserver:obj forKeyPath:@"name" options:nil context:@"testRemoveAllObservers"];
[bob addObserver:obj2 forKeyPath:@"name" options:nil context:@"testRemoveAllObservers"];
[bob addObserver:obj forKeyPath:@"name" options:0 context:@"testRemoveAllObservers"];
[bob addObserver:obj2 forKeyPath:@"name" options:0 context:@"testRemoveAllObservers"];
[bob removeObserver:obj forKeyPath:@"name"];
[bob removeObserver:obj2 forKeyPath:@"name"];
@@ -133,7 +133,7 @@
[dict addObserver:self
forKeyPath:@"dictionaryKey"
options:nil
options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld
context:@"testDictionary"];
[dict setObject:@"Jo Bob Ray" forKey:@"dictionaryKey"];
@@ -147,7 +147,20 @@
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionPrior context:@"testPriorObservationOption"];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionPrior | CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testPriorObservationOption"];
[bob setValue:@"bob" forKey:@"name"];
[self assertTrue:_sawPriorObservation message:@"asked for CPKeyValueObservingOptionPrior but did not recieve corresponding notification"];
}
- (void)testPriorObservationOptionNoOld
{
_sawPriorObservation = NO;
bob = [[PersonTester alloc] init];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionPrior | CPKeyValueObservingOptionNew context:@"testPriorObservationOptionNoOld"];
[bob setValue:@"bob" forKey:@"name"];
@@ -162,7 +175,23 @@
bob.name = "paul";
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionInitial context:@"testInitialObservationOption"];
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionInitial | CPKeyValueObservingOptionNew context:@"testInitialObservationOption"];
[bob removeObserver:self forKeyPath:@"name"];
[bob setValue:@"bob" forKey:@"name"];
[self assertTrue:_sawInitialObservation message:@"asked for CPKeyValueObservingOptionInitial but did not recieve corresponding notification"];
}
- (void)testInitialObservationOptionNoNew
{
_sawInitialObservation = NO;
bob = [[PersonTester alloc] init];
bob.name = "paul";
[bob addObserver:self forKeyPath:@"name" options:CPKeyValueObservingOptionInitial context:@"testInitialObservationOptionNoNew"];
[bob removeObserver:self forKeyPath:@"name"];
[bob setValue:@"bob" forKey:@"name"];
@@ -178,8 +207,8 @@
bob.name = "paul";
[bob addObserver:self forKeyPath:@"bobName" options:0 context:@"testDependentKeyObservation"];
[bob addObserver:self forKeyPath:@"twiceRemoved" options:0 context:@"testDependentKeyObservation2"];
[bob addObserver:self forKeyPath:@"bobName" options:CPKeyValueObservingOptionOld context:@"testDependentKeyObservation"];
[bob addObserver:self forKeyPath:@"twiceRemoved" options:CPKeyValueObservingOptionOld context:@"testDependentKeyObservation2"];
[bob setValue:@"bob" forKey:@"name"];
@@ -194,7 +223,7 @@
[cs101 setTeacher:bob];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:0 context:@"testMultipartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.name" options:CPKeyValueObservingOptionNew context:@"testMultipartKey"];
[bob setName:@"bob"];
@@ -209,7 +238,7 @@
[bob setValue:focus forKey:@"car"];
[bob addObserver:self forKeyPath:@"self.car.thisCar.model" options:0 context:@"testMultiPartKeysWhereValuesEvaluateToSelf"];
[bob addObserver:self forKeyPath:@"self.car.thisCar.model" options:CPKeyValueObservingOptionNew context:@"testMultiPartKeysWhereValuesEvaluateToSelf"];
[focus setValue:@"ford focus" forKey:@"model"];
@@ -225,7 +254,7 @@
[cs101 setTeacher:bob];
[bob setValue:focus forKey:@"car"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:0 context:@"testThreePartKey"];
[cs101 addObserver:self forKeyPath:@"teacher.car.model" options:CPKeyValueObservingOptionNew context:@"testThreePartKey"];
[focus setValue:@"ford focus" forKey:@"model"];
[self assertTrue: _sawObservation message:@"Never recieved an observation"];
@@ -240,7 +269,7 @@
[cs101 setTeacher:bob];
[focus setValue:@"2000" forKey:@"year"];
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:0 context:@"testThreePartKeyPart2"];
[cs101 addObserver:self forKeyPath:@"teacher.car.year" options:CPKeyValueObservingOptionNew context:@"testThreePartKeyPart2"];
[bob setValue:focus forKey:@"car"];
@@ -290,7 +319,7 @@
[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:0 context:@"testCrazyKeyPathChanges"];
[a addObserver:self forKeyPath:@"b.c.d.e.f" options:CPKeyValueObservingOptionNew context:@"testCrazyKeyPathChanges"];
var newD = [D new];
@@ -311,7 +340,7 @@
[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:0 context:@"testCrazyKeyPathChanges2"];
[a addObserver:self forKeyPath:@"b.c.d.e.f" options:CPKeyValueObservingOptionNew context:@"testCrazyKeyPathChanges2"];
[a setValue:nil forKeyPath:@"b.c"];
@@ -328,7 +357,7 @@
[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:0 context:@"testCrazyKeyPathChanges3"];
[a addObserver:self forKeyPath:@"b.c.d.e.f" options:CPKeyValueObservingOptionNew context:@"testCrazyKeyPathChanges3"];
[a setValue:7 forKeyPath:@"b.c.d.e.f"];
@@ -341,7 +370,7 @@
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:@"testInsertIntoToManyProperty"];
[tester addObserver:self forKeyPath:@"managedObjects" options:CPKeyValueObservingOptionNew context:@"testInsertIntoToManyProperty"];
[tester insertObject:5 inManagedObjectsAtIndex:4];
@@ -354,7 +383,7 @@
[tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
[tester addObserver:self forKeyPath:@"managedObjects" options:0 context:@"testRemoveFromToManyProperty"];
[tester addObserver:self forKeyPath:@"managedObjects" options:CPKeyValueObservingOptionOld context:@"testRemoveFromToManyProperty"];
[tester removeObjectFromManagedObjectsAtIndex:0];
@@ -367,7 +396,7 @@
tester.tester = [ToManyTester new];
[tester.tester setValue:[1, 2, 3, 4] forKey:@"managedObjects"];
[tester addObserver:self forKeyPath:@"tester.managedObjects" options:0 context:@"testInsertIntoToManyPropertyIndirectly"];
[tester addObserver:self forKeyPath:@"tester.managedObjects" options:CPKeyValueObservingOptionNew context:@"testInsertIntoToManyPropertyIndirectly"];
[tester.tester insertObject:5 inManagedObjectsAtIndex:4];
@@ -381,7 +410,7 @@
tester.tester = [ToManyTester new];
[tester.tester setValue:[1, 2, 3, 4] forKey:@"subviews"];
[tester addObserver:self forKeyPath:@"tester.subviews" options:0 context:@"testInsertIntoArrayPropertyIndirectly"];
[tester addObserver:self forKeyPath:@"tester.subviews" options:CPKeyValueObservingOptionNew context:@"testInsertIntoArrayPropertyIndirectly"];
[tester.tester insertSubview:5 atIndex:4];
@@ -395,7 +424,7 @@
tester.tester = [ToManyTester new];
[tester.tester setValue:[1, 2, 3, 4] forKey:@"subviews"];
[tester addObserver:self forKeyPath:@"tester.subviews" options:0 context:@"testChangeTopLevelObject"];
[tester addObserver:self forKeyPath:@"tester.subviews" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:@"testChangeTopLevelObject"];
var newTesterTester = [ToManyTester new];
[newTesterTester setValue:[5, 6, 7, 8] forKey:@"subviews"];
@@ -554,7 +583,24 @@
if (!_sawPriorObservation)
{
[self assertTrue:prior message:@"Have not been sent the prior notification, but it should have been sent"];
[self assertTrue:oldValue == [CPNull null] message:@"Shoudl be no initial value"];
[self assertTrue:oldValue == [CPNull null] message:@"Should be no initial value"];
[self assertFalse:newValue message:@"Should be no object for the new value key on the prior notification"];
_sawPriorObservation = YES;
}
else
{
[self assertFalse:prior message:@"there should be no value for the notification is prior key, that notification was already sent"];
[self assertTrue:newValue == "set_bob" message:@"newValue should be: set_bob was: " + newValue];
}
break;
case "testPriorObservationOptionNoOld":
var prior = [changes objectForKey:CPKeyValueChangeNotificationIsPriorKey];
if (!_sawPriorObservation)
{
[self assertTrue:prior message:@"Have not been sent the prior notification, but it should have been sent"];
[self assertTrue:oldValue == nil message:@"Should be no old value"];
[self assertFalse:newValue message:@"Should be no object for the new value key on the prior notification"];
_sawPriorObservation = YES;
}
@@ -568,8 +614,20 @@
case "testInitialObservationOption":
if (!_sawInitialObservation)
{
[self assertTrue:newValue == "paul" message:@"Expected old value to be: paul was: " + oldValue];
[self assertFalse:oldValue message:@"Should be no value for new change key on initial observation"];
[self assertTrue:newValue == "paul" message:@"Expected new value to be: paul was: " + newValue];
[self assertTrue:oldValue == nil message:@"Should be no value for old change key on initial observation"];
_sawInitialObservation = YES;
}
else
[self assertFalse:YES message:@"Should never have received this notification"];
break;
case "testInitialObservationOptionNoNew":
if (!_sawInitialObservation)
{
[self assertTrue:newValue == nil message:@"New value should not be included"];
[self assertTrue:oldValue == nil message:@"Should be no value for old change key on initial observation"];
_sawInitialObservation = YES;
}
else
+236 -4
View File
@@ -2,6 +2,7 @@
@import <Foundation/CPKeyValueCoding.j>
@import <Foundation/CPKeyValueObserving.j>
var _getCheeseCounter;
@implementation CPKeyValueObservingTest : OJTestCase
{
@@ -9,6 +10,12 @@
id _lastObject;
CPDictionary _lastChange;
id _lastContext;
CPString _secondLastKeyPath;
id _secondLastObject;
CPDictionary _secondLastChange;
id _secondLastContext;
}
- (Class)objectWithMethods:(CPString)aMethodName, ...
@@ -26,9 +33,10 @@
return [theClass new];
}
- (void)setup
- (void)setUp
{
_lastKeyPath = _lastObject = _lastChange = _lastContext = nil;
_lastKeyPath = _lastObject = _lastChange = _lastContext = _secondLastKeyPath = _secondLastObject = _secondLastChange = _secondLastContext = nil;
_getCheeseCounter = 0;
}
- (void)testInitialObserving
@@ -38,18 +46,210 @@
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:[CPDictionary dictionaryWithObject:@"CHEESE!" forKey:CPKeyValueChangeNewKey] equals:_lastChange];
[self assert:@{CPKeyValueChangeNewKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
}
- (void)testInitialObservingNoNew
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionInitial context:nil];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
}
- (void)testSettingObservingPrior
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionPrior context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_secondLastKeyPath];
[self assert:tester equals:_secondLastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNotificationIsPriorKey: 1} equals:_secondLastChange];
[self assert:nil equals:_secondLastContext];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:0 equals:_getCheeseCounter message:@"Get method should never be called"];
}
- (void)testSettingObservingPriorPlusMoreObservers
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionPrior context:nil];
var observerWithOld = [[AnotherObserver alloc] init];
[tester addObserver:observerWithOld forKeyPath:@"cheese" options:CPKeyValueObservingOptionPrior | CPKeyValueObservingOptionOld context:nil];
var observerWithNew = [[AnotherObserver alloc] init];
[tester addObserver:observerWithNew forKeyPath:@"cheese" options:CPKeyValueObservingOptionPrior | CPKeyValueObservingOptionNew context:nil];
var observerWithNewAndOld = [[AnotherObserver alloc] init];
[tester addObserver:observerWithNewAndOld forKeyPath:@"cheese" options:CPKeyValueObservingOptionPrior | CPKeyValueObservingOptionOld | CPKeyValueObservingOptionNew context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_secondLastKeyPath];
[self assert:tester equals:_secondLastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNotificationIsPriorKey: 1} equals:_secondLastChange];
[self assert:nil equals:_secondLastContext];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:@"cheese" equals:observerWithOld._secondLastKeyPath];
[self assert:tester equals:observerWithOld._secondLastObject];
[self assert:@{CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNotificationIsPriorKey: 1} equals:observerWithOld._secondLastChange];
[self assert:nil equals:observerWithOld._secondLastContext];
[self assert:@"cheese" equals:observerWithOld._lastKeyPath];
[self assert:tester equals:observerWithOld._lastObject];
[self assert:@{CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:observerWithOld._lastChange];
[self assert:nil equals:observerWithOld._lastContext];
[self assert:@"cheese" equals:observerWithNew._secondLastKeyPath];
[self assert:tester equals:observerWithNew._secondLastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNotificationIsPriorKey: 1} equals:observerWithNew._secondLastChange];
[self assert:nil equals:observerWithNew._secondLastContext];
[self assert:@"cheese" equals:observerWithNew._lastKeyPath];
[self assert:tester equals:observerWithNew._lastObject];
[self assert:@{CPKeyValueChangeNewKey: @"NEW CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:observerWithNew._lastChange];
[self assert:nil equals:observerWithNew._lastContext];
[self assert:@"cheese" equals:observerWithNewAndOld._secondLastKeyPath];
[self assert:tester equals:observerWithNewAndOld._secondLastObject];
[self assert:@{CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting, CPKeyValueChangeNotificationIsPriorKey: 1} equals:observerWithNewAndOld._secondLastChange];
[self assert:nil equals:observerWithNewAndOld._secondLastContext];
[self assert:@"cheese" equals:observerWithNewAndOld._lastKeyPath];
[self assert:tester equals:observerWithNewAndOld._lastObject];
[self assert:@{CPKeyValueChangeNewKey: @"NEW CHEESE!", CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:observerWithNewAndOld._lastChange];
[self assert:nil equals:observerWithNewAndOld._lastContext];
[self assert:2 equals:_getCheeseCounter message:@"Get method should only be called twice, not " + _getCheeseCounter + " times"];
}
- (void)testSettingObserving
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:0 context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:0 equals:_getCheeseCounter message:@"Get method should never be called"];
}
- (void)testSettingObservingNewPlusAnotherObserverWithNoNew
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionNew context:nil];
var anotherObserver = [[AnotherObserver alloc] init];
[tester addObserver:anotherObserver forKeyPath:@"cheese" options:0 context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeNewKey: @"NEW CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:@"cheese" equals:anotherObserver._lastKeyPath];
[self assert:tester equals:anotherObserver._lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:anotherObserver._lastChange];
[self assert:nil equals:anotherObserver._lastContext];
[self assert:1 equals:_getCheeseCounter message:@"Get method should only be called once, not " + _getCheeseCounter + " times"];
}
- (void)testSettingObservingOldPlusAnotherObserverWithNoOld
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionOld context:nil];
var anotherObserver = [[AnotherObserver alloc] init];
[tester addObserver:anotherObserver forKeyPath:@"cheese" options:0 context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:@"cheese" equals:anotherObserver._lastKeyPath];
[self assert:tester equals:anotherObserver._lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:anotherObserver._lastChange];
[self assert:nil equals:anotherObserver._lastContext];
[self assert:1 equals:_getCheeseCounter message:@"Get method should only be called once, not " + _getCheeseCounter + " times"];
}
- (void)testSettingObservingNewAndOldPlusMoreObserversWithOnlyNewOrOld
{
var tester = [ObservingTester testerWithCheese:@"CHEESE!"];
[tester addObserver:self forKeyPath:@"cheese" options:CPKeyValueObservingOptionNew | CPKeyValueObservingOptionOld context:nil];
var anotherObserver = [[AnotherObserver alloc] init];
[tester addObserver:anotherObserver forKeyPath:@"cheese" options:CPKeyValueObservingOptionOld context:nil];
var yetAnotherObserver = [[AnotherObserver alloc] init];
[tester addObserver:yetAnotherObserver forKeyPath:@"cheese" options:CPKeyValueObservingOptionNew context:nil];
var noNewOrOldObserver = [[AnotherObserver alloc] init];
[tester addObserver:noNewOrOldObserver forKeyPath:@"cheese" options:0 context:nil];
[tester setValue:@"NEW CHEESE!" forKey:@"cheese"];
[self assert:@"cheese" equals:_lastKeyPath];
[self assert:tester equals:_lastObject];
[self assert:@{CPKeyValueChangeNewKey: @"NEW CHEESE!", CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:_lastChange];
[self assert:nil equals:_lastContext];
[self assert:@"cheese" equals:anotherObserver._lastKeyPath];
[self assert:tester equals:anotherObserver._lastObject];
[self assert:@{CPKeyValueChangeOldKey: @"CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:anotherObserver._lastChange];
[self assert:nil equals:anotherObserver._lastContext];
[self assert:@"cheese" equals:yetAnotherObserver._lastKeyPath];
[self assert:tester equals:yetAnotherObserver._lastObject];
[self assert:@{CPKeyValueChangeNewKey: @"NEW CHEESE!", CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:yetAnotherObserver._lastChange];
[self assert:nil equals:yetAnotherObserver._lastContext];
[self assert:@"cheese" equals:noNewOrOldObserver._lastKeyPath];
[self assert:tester equals:noNewOrOldObserver._lastObject];
[self assert:@{CPKeyValueChangeKindKey: CPKeyValueChangeSetting} equals:noNewOrOldObserver._lastChange];
[self assert:nil equals:noNewOrOldObserver._lastContext];
[self assert:2 equals:_getCheeseCounter message:@"Get method should only be called twice even with many observers, not " + _getCheeseCounter + " times"];
}
- (void)observeValueForKeyPath:(CPString)aKeyPath
ofObject:(id)anObject
change:(CPDictionary)aChange
context:(id)aContext
{
_secondLastKeyPath = _lastKeyPath;
_secondLastObject = _lastObject;
_secondLastChange = _lastChange;
_secondLastContext = _lastContext;
_lastKeyPath = aKeyPath;
_lastObject = anObject;
_lastChange = aChange;
_lastChange = [aChange copy];
_lastContext = aContext;
}
@@ -244,6 +444,7 @@
- (id)cheese
{
_getCheeseCounter++;
return cheese;
}
@@ -280,3 +481,34 @@
}
@end
@implementation AnotherObserver : CPObject
{
CPString _lastKeyPath;
id _lastObject;
CPDictionary _lastChange;
id _lastContext;
CPString _secondLastKeyPath;
id _secondLastObject;
CPDictionary _secondLastChange;
id _secondLastContext;
}
- (void)observeValueForKeyPath:(CPString)aKeyPath
ofObject:(id)anObject
change:(CPDictionary)aChange
context:(id)aContext
{
_secondLastKeyPath = _lastKeyPath;
_secondLastObject = _lastObject;
_secondLastChange = _lastChange;
_secondLastContext = _lastContext;
_lastKeyPath = aKeyPath;
_lastObject = anObject;
_lastChange = [aChange copy];
_lastContext = aContext;
}
@end