Skip to content
This repository has been archived by the owner on Mar 3, 2020. It is now read-only.

Custom NSKeyValueObservingOptions to add FBKVONotificationKeyPathKey #126

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions FBKVOController/FBKVOController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
27 changes: 15 additions & 12 deletions FBKVOController/FBKVOController.m
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@
case NSKeyValueObservingOptionPrior:
return @"NSKeyValueObservingOptionPrior";
break;
case FBKeyValueObservingOptionKeyPath:
return @"FBKeyValueObservingOptionKeyPath";
break;
default:
NSCAssert(NO, @"unexpected option %tu", option);
break;
Expand Down Expand Up @@ -372,23 +375,23 @@ - (void)observeValueForKeyPath:(nullable NSString *)keyPath
id observer = controller.observer;
if (nil != observer) {

NSDictionary<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath && (info->_options & FBKeyValueObservingOptionKeyPath) != 0) {
NSMutableDictionary<NSKeyValueChangeKey, id> *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<NSKeyValueChangeKey, id> *changeWithKeyPath = change;
// add the keyPath to the change dictionary for clarity when mulitple keyPaths are being observed
if (keyPath) {
NSMutableDictionary<NSString *, id> *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];
}
}
}
Expand Down Expand Up @@ -590,7 +593,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)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];
}
}

Expand Down Expand Up @@ -618,7 +621,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)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];
}
}

Expand All @@ -644,7 +647,7 @@ - (void)observe:(nullable id)object keyPaths:(NSArray<NSString *> *)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];
}
}

Expand Down
49 changes: 48 additions & 1 deletion FBKVOControllerTests/FBKVOControllerTests.m
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<FBKVOTestObserving> 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);
Expand Down Expand Up @@ -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]];
}];

Expand All @@ -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]));
}

Expand Down