From 2263c660dfb9bc26ff8e70229da68d0888a1516e Mon Sep 17 00:00:00 2001 From: "Dr. Kibitz" Date: Tue, 7 Mar 2017 00:25:53 -0800 Subject: [PATCH 1/2] Custom NSKeyValueObservingOptions to add FBKVONotificationKeyPathKey --- FBKVOController/FBKVOController.h | 9 +++- FBKVOController/FBKVOController.m | 31 +++++++------ FBKVOControllerTests/FBKVOControllerTests.m | 49 ++++++++++++++++++++- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/FBKVOController/FBKVOController.h b/FBKVOController/FBKVOController.h index 285c109..72ff247 100644 --- a/FBKVOController/FBKVOController.h +++ b/FBKVOController/FBKVOController.h @@ -39,10 +39,17 @@ 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 */ -extern NSString *const FBKVONotificationKeyPathKey; +extern NSKeyValueChangeKey const FBKVONotificationKeyPathKey; /** @abstract Block called on key-value change notification. diff --git a/FBKVOController/FBKVOController.m b/FBKVOController/FBKVOController.m index 8b48a08..ca2920d 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; @@ -93,7 +96,7 @@ typedef NS_ENUM(uint8_t, _FBKVOInfoState) { _FBKVOInfoStateNotObserving, }; -NSString *const FBKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey"; +NSKeyValueChangeKey const FBKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey"; /** @abstract The key-value observation info. @@ -348,7 +351,7 @@ - (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object - change:(nullable NSDictionary *)change + change:(nullable NSDictionary *)change context:(nullable void *)context { NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change); @@ -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])); } From 6cf3b36d1d411416e1d6ce7319254b61d22077d5 Mon Sep 17 00:00:00 2001 From: "Dr. Kibitz" Date: Tue, 7 Mar 2017 01:29:33 -0800 Subject: [PATCH 2/2] Pull back on some changes already taken care of in approved #124 --- FBKVOController/FBKVOController.h | 2 +- FBKVOController/FBKVOController.m | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/FBKVOController/FBKVOController.h b/FBKVOController/FBKVOController.h index 72ff247..4ab0b5e 100644 --- a/FBKVOController/FBKVOController.h +++ b/FBKVOController/FBKVOController.h @@ -49,7 +49,7 @@ static NSKeyValueObservingOptions const FBKeyValueObservingOptionKeyPath = (1 << /** Key provided in the @c change dictionary of @c FBKVONotificationBlock that's value represents the key-path being observed */ -extern NSKeyValueChangeKey const FBKVONotificationKeyPathKey; +extern NSString *const FBKVONotificationKeyPathKey; /** @abstract Block called on key-value change notification. diff --git a/FBKVOController/FBKVOController.m b/FBKVOController/FBKVOController.m index ca2920d..65d5c36 100644 --- a/FBKVOController/FBKVOController.m +++ b/FBKVOController/FBKVOController.m @@ -96,7 +96,7 @@ typedef NS_ENUM(uint8_t, _FBKVOInfoState) { _FBKVOInfoStateNotObserving, }; -NSKeyValueChangeKey const FBKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey"; +NSString *const FBKVONotificationKeyPathKey = @"FBKVONotificationKeyPathKey"; /** @abstract The key-value observation info. @@ -351,7 +351,7 @@ - (void)unobserve:(id)object infos:(nullable NSSet<_FBKVOInfo *> *)infos - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object - change:(nullable NSDictionary *)change + change:(nullable NSDictionary *)change context:(nullable void *)context { NSAssert(context, @"missing context keyPath:%@ object:%@ change:%@", keyPath, object, change); @@ -375,7 +375,7 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath id observer = controller.observer; if (nil != observer) { - NSDictionary *changeWithKeyPath = change; + 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];