diff --git a/FBKVOController/FBKVOController.h b/FBKVOController/FBKVOController.h index 64a030d..43866c7 100644 --- a/FBKVOController/FBKVOController.h +++ b/FBKVOController/FBKVOController.h @@ -39,6 +39,13 @@ NS_ASSUME_NONNULL_BEGIN +/** + Additional option that may be passed to @c NSKeyValueObservingOptions mask. + When provided, and observing a keyPath, the @c FBKVONotificationKeyPathKey key will be added to the @c change dictionary. + This option is added automatically for any method passing multiple keyPaths. + */ +static NSKeyValueObservingOptions const FBKeyValueObservingOptionKeyPath = (1 << NSKeyValueObservingOptionPrior); + /** Key provided in the @c change dictionary of @c FBKVONotificationBlock that's value represents the key-path being observed */ diff --git a/FBKVOController/FBKVOController.m b/FBKVOController/FBKVOController.m index 02310c2..329efaf 100644 --- a/FBKVOController/FBKVOController.m +++ b/FBKVOController/FBKVOController.m @@ -35,6 +35,9 @@ case NSKeyValueObservingOptionPrior: return @"NSKeyValueObservingOptionPrior"; break; + case FBKeyValueObservingOptionKeyPath: + return @"FBKeyValueObservingOptionKeyPath"; + break; default: NSCAssert(NO, @"unexpected option %tu", option); break; @@ -372,23 +375,23 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath id observer = controller.observer; if (nil != observer) { + NSDictionary *changeWithKeyPath = change; + // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed + if (keyPath && (info->_options & FBKeyValueObservingOptionKeyPath) != 0) { + NSMutableDictionary *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey]; + [mChange addEntriesFromDictionary:change]; + changeWithKeyPath = [mChange copy]; + } // dispatch custom block or action, fall back to default action if (info->_block) { - NSDictionary *changeWithKeyPath = change; - // add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed - if (keyPath) { - NSMutableDictionary *mChange = [NSMutableDictionary dictionaryWithObject:keyPath forKey:FBKVONotificationKeyPathKey]; - [mChange addEntriesFromDictionary:change]; - changeWithKeyPath = [mChange copy]; - } info->_block(observer, object, changeWithKeyPath); } else if (info->_action) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" - [observer performSelector:info->_action withObject:change withObject:object]; + [observer performSelector:info->_action withObject:changeWithKeyPath withObject:object]; #pragma clang diagnostic pop } else { - [observer observeValueForKeyPath:keyPath ofObject:object change:change context:info->_context]; + [observer observeValueForKeyPath:keyPath ofObject:object change:changeWithKeyPath context:info->_context]; } } } @@ -590,7 +593,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray *)keyPaths opt } for (NSString *keyPath in keyPaths) { - [self observe:object keyPath:keyPath options:options block:block]; + [self observe:object keyPath:keyPath options:options|FBKeyValueObservingOptionKeyPath block:block]; } } @@ -618,7 +621,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray *)keyPaths opt } for (NSString *keyPath in keyPaths) { - [self observe:object keyPath:keyPath options:options action:action]; + [self observe:object keyPath:keyPath options:options|FBKeyValueObservingOptionKeyPath action:action]; } } @@ -644,7 +647,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray *)keyPaths opt } for (NSString *keyPath in keyPaths) { - [self observe:object keyPath:keyPath options:options context:context]; + [self observe:object keyPath:keyPath options:options|FBKeyValueObservingOptionKeyPath context:context]; } } diff --git a/FBKVOControllerTests/FBKVOControllerTests.m b/FBKVOControllerTests/FBKVOControllerTests.m index 2b3083f..5fd3255 100644 --- a/FBKVOControllerTests/FBKVOControllerTests.m +++ b/FBKVOControllerTests/FBKVOControllerTests.m @@ -30,6 +30,7 @@ @implementation FBKVOControllerTests static void *context = (void *)@"context"; static NSKeyValueObservingOptions const optionsNone = 0; static NSKeyValueObservingOptions const optionsBasic = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld | NSKeyValueObservingOptionInitial; +static NSKeyValueObservingOptions const optionsBasicWithCustomOption = optionsBasic | FBKeyValueObservingOptionKeyPath; static NSKeyValueObservingOptions const optionsAll = optionsBasic | NSKeyValueObservingOptionPrior; - (void)testDebugDescriptionContainsClassName @@ -67,13 +68,56 @@ - (void)testBlockOptionsBasic blockKeyPath = change[FBKVONotificationKeyPathKey]; blockCallCount++; }]; + XCTAssert(1 == blockCallCount, @"unexpected block call count:%lu expected:%d", (unsigned long)blockCallCount, 1); + XCTAssert(blockObserver == observer, @"value:%@ expected:%@", blockObserver, observer); + XCTAssert(blockObject == referenceObserver.lastObject, @"value:%@ expected:%@", blockObject, referenceObserver.lastObject); + XCTAssertNil(blockKeyPath, @"value:%@ expected:nil", blockKeyPath); + XCTAssertEqualObjects(blockChange, referenceObserver.lastChange, @"value:%@ expected:%@", blockChange, referenceObserver.lastChange); + + circle.radius = 1.0; + XCTAssert(2 == blockCallCount, @"unexpected block call count:%lu expected:%d", (unsigned long)blockCallCount, 2); + XCTAssert(blockObserver == observer, @"value:%@ expected:%@", blockObserver, observer); + XCTAssert(blockObject == referenceObserver.lastObject, @"value:%@ expected:%@", blockObject, referenceObserver.lastObject); + XCTAssertNil(blockKeyPath, @"value:%@ expected:nil", blockKeyPath); + XCTAssertEqualObjects(blockChange, referenceObserver.lastChange, @"value:%@ expected:%@", blockChange, referenceObserver.lastChange); + // cleanup + [circle removeObserver:referenceObserver forKeyPath:radius]; +} + +- (void)testBlockOptionsBasicWithCustomOption +{ + FBKVOTestCircle *circle = [FBKVOTestCircle circle]; + id observer = mockProtocol(@protocol(FBKVOTestObserving)); + FBKVOController *controller = [FBKVOController controllerWithObserver:observer]; + FBKVOTestObserver *referenceObserver = [FBKVOTestObserver observer]; + + // add reference observe + [circle addObserver:referenceObserver forKeyPath:radius options:optionsBasicWithCustomOption context:context]; + + __block NSUInteger blockCallCount = 0; + __block id blockObserver = nil; + __block id blockObject = nil; + __block NSString *blockKeyPath = nil; + __block NSDictionary *blockChange = nil; + + // add mock observer + [controller observe:circle keyPath:radius options:optionsBasicWithCustomOption block:^(id observer, id object, NSDictionary *change) { + blockObserver = observer; + blockObject = object; + NSMutableDictionary *mChange = [change mutableCopy]; + [mChange removeObjectForKey:FBKVONotificationKeyPathKey]; + blockChange = [mChange copy]; + blockKeyPath = change[FBKVONotificationKeyPathKey]; + blockCallCount++; + }]; + XCTAssert(1 == blockCallCount, @"unexpected block call count:%lu expected:%d", (unsigned long)blockCallCount, 1); XCTAssert(blockObserver == observer, @"value:%@ expected:%@", blockObserver, observer); XCTAssert(blockObject == referenceObserver.lastObject, @"value:%@ expected:%@", blockObject, referenceObserver.lastObject); XCTAssert([blockKeyPath isEqualToString:radius], @"value:%@ expected:%@", blockKeyPath, radius); XCTAssertEqualObjects(blockChange, referenceObserver.lastChange, @"value:%@ expected:%@", blockChange, referenceObserver.lastChange); - + circle.radius = 1.0; XCTAssert(2 == blockCallCount, @"unexpected block call count:%lu expected:%d", (unsigned long)blockCallCount, 2); XCTAssert(blockObserver == observer, @"value:%@ expected:%@", blockObserver, observer); @@ -195,9 +239,11 @@ - (void)testObserveKeyPathsOptionsBlockObservesEachOfTheKeyPaths // Arrange 2: Observe the key paths "radius" and "borderWidth" on the circle. // Aggregate the new values in an array. + NSMutableArray *observedKeys = [NSMutableArray array]; NSMutableArray *newValues = [NSMutableArray array]; FBKVOTestCircle *circle = [FBKVOTestCircle circle]; [controller observe:circle keyPaths:@[radius, borderWidth] options:NSKeyValueObservingOptionNew block:^(id observer, FBKVOTestCircle *circle, NSDictionary *change) { + [observedKeys addObject:change[FBKVONotificationKeyPathKey]]; [newValues addObject:change[NSKeyValueChangeNewKey]]; }]; @@ -206,6 +252,7 @@ - (void)testObserveKeyPathsOptionsBlockObservesEachOfTheKeyPaths circle.borderWidth = 10.f; // Assert: Changes to the radius and borderWidth should have been observed. + assertThat(observedKeys, equalTo(@[radius, borderWidth])); assertThat(newValues, equalTo(@[@1, @10])); }