From 5e0e61b7e50bfaaafb3f12c37028f53ce760d386 Mon Sep 17 00:00:00 2001 From: Martin Carlberg Date: Thu, 9 Oct 2014 19:25:56 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Don=E2=80=99t=20create=20old=20or=20new=20v?= =?UTF-8?q?alues=20in=20KVO=20change=20dictionary=20when=20doing=20binding?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AppKit/CPKeyValueBinding.j | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AppKit/CPKeyValueBinding.j b/AppKit/CPKeyValueBinding.j index 0c9bb22f0..1913a4436 100644 --- a/AppKit/CPKeyValueBinding.j +++ b/AppKit/CPKeyValueBinding.j @@ -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]]; From 643566bbfd255e6e68b14a42394dfe5f133b8bc6 Mon Sep 17 00:00:00 2001 From: Martin Carlberg Date: Thu, 9 Oct 2014 19:27:57 +0200 Subject: [PATCH 2/3] Honor options when creating KVO change dictionary --- Foundation/CPKeyValueObserving.j | 265 ++++++++++++++++++++----------- 1 file changed, 172 insertions(+), 93 deletions(-) diff --git a/Foundation/CPKeyValueObserving.j b/Foundation/CPKeyValueObserving.j index 5dc1257a0..53f110b32 100644 --- a/Foundation/CPKeyValueObserving.j +++ b/Foundation/CPKeyValueObserving.j @@ -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]; From 5b7afd72e3ee8309ff7920721a92a52b14c7b711 Mon Sep 17 00:00:00 2001 From: Martin Carlberg Date: Thu, 9 Oct 2014 19:30:05 +0200 Subject: [PATCH 3/3] Fixed: Test cases now use correct options for KVO change dictionaries --- Tests/AppKit/CPArrayControllerTest.j | 85 ++++++++ Tests/Foundation/CPKVOTest.j | 120 ++++++++--- Tests/Foundation/CPKeyValueObservingTest.j | 240 ++++++++++++++++++++- 3 files changed, 410 insertions(+), 35 deletions(-) diff --git a/Tests/AppKit/CPArrayControllerTest.j b/Tests/AppKit/CPArrayControllerTest.j index 5e7f8f982..3365cfdcf 100644 --- a/Tests/AppKit/CPArrayControllerTest.j +++ b/Tests/AppKit/CPArrayControllerTest.j @@ -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:@"", [self name]]; } +- (CPString)name +{ + countGetName++; + return _name; +} + @end @implementation Department : CPObject diff --git a/Tests/Foundation/CPKVOTest.j b/Tests/Foundation/CPKVOTest.j index 0a4bccba5..1fa905abd 100644 --- a/Tests/Foundation/CPKVOTest.j +++ b/Tests/Foundation/CPKVOTest.j @@ -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 diff --git a/Tests/Foundation/CPKeyValueObservingTest.j b/Tests/Foundation/CPKeyValueObservingTest.j index 82a0e4a7e..9726a7f05 100644 --- a/Tests/Foundation/CPKeyValueObservingTest.j +++ b/Tests/Foundation/CPKeyValueObservingTest.j @@ -2,6 +2,7 @@ @import @import +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