2
votes

I'm interested in registering my view controller for KVO notifications on a model object's properties.

The "member" property of the view controller is an NSManagedObject subclass and uses Core Data's provided accessor methods (via @dynamic). It has four properties: firstName, lastName, nickname, and bio, which are all NSStrings.

Here's the registering and unregistering for KVO:

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    [self.member addObserver:self
                  forKeyPath:@"firstName"
                     options:NSKeyValueObservingOptionNew
                     context:kFHMemberDetailContext];

    [self.member addObserver:self
                  forKeyPath:@"lastName"
                     options:NSKeyValueObservingOptionNew
                     context:kMemberDetailContext];

    [self.member addObserver:self
                  forKeyPath:@"nickname"
                     options:NSKeyValueObservingOptionNew
                     context:kMemberDetailContext];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];

    [self.member removeObserver:self forKeyPath:@"firstName" context:kMemberDetailContext];
    [self.member removeObserver:self forKeyPath:@"lastName"  context:kMemberDetailContext];
    [self.member removeObserver:self forKeyPath:@"nickname"  context:kMemberDetailContext];
}

Implementation of callback method

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context != kFHMemberDetailContext) {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        return;
    }

    self.kvoCount++;

    if ([keyPath isEqualToString:@"firstName"]) {
       NSLog(@"firstName KVO'd");
    }
    else if ([keyPath isEqualToString:@"lastName"]) {
       NSLog(@"lastName KVO'd");
    }
    else if ([keyPath isEqualToString:@"nickname"]) {
       NSLog(@"nickname KVO'd");
    }
}

When I drive this code from a unit test, I receive three notifications when I modify the "bio" property, and four notifications when modifying firstName, lastName, or nickname. That's consistently three too many notifications!

It seems to be something simple that I've done wrong, but I can't figure out what's causing the extraneous notifications. If I po the change dictionary, the NSKeyValueChangeKindKey is always NSKeyValueChangeSetting, but for the unwanted notifications the NSKeyValueChangeNewKey is NULL.

The test driving this bit of code:

- (void)setUp
{
    NSManagedObjectModel *mom = [NSManagedObjectModel mergedModelFromBundles:@[[NSBundle mainBundle]]];
    NSPersistentStoreCoordinator *psc = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:mom];
    [psc addPersistentStoreWithType:NSInMemoryStoreType
                      configuration:nil
                                URL:nil
                            options:nil
                              error:NULL];
    NSManagedObjectContext *managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
    [managedObjectContext setPersistentStoreCoordinator:psc];
    member = [NSEntityDescription insertNewObjectForEntityForName:@"Member" inManagedObjectContext:managedObjectContext];

    UIStoryboard *sb = [UIStoryboard storyboardWithName:@"MainStoryboard_iPhone" bundle:[NSBundle mainBundle]];
    memberDetailVC = [sb instantiateViewControllerWithIdentifier:kFHMemberDetailTableViewControllerIdentifier];
    [memberDetailVC setMember:member];
}

- (void)tearDown
{
    [memberDetailVC viewWillDisappear:NO];
}

- (void)testChangesToMemberFirstNamePropertyCausesKVO
{
    [memberDetailVC viewWillAppear:NO];

    [member setFirstName:@"Unit Test"];

    STAssertTrue(memberDetailVC.kvoCount, (NSInteger)1, @"View controller should have received a single KVO notification");
}

Like I said, this fails having received 4 notifications (one for each property with a new value of null, and then finally the expected notification).

2

2 Answers

2
votes

So the answer to my problem is an issue with my unit test. In my setup method, I'm creating the Managed Object Context, inserting a Managed Object into it, and then the context is getting dealloc'd at the end of my -setUp method.

If I hold onto the MOC as an ivar in the test suite, the notifications come in as expected.

This raises all kind of other questions, but I'll leave those for some other time. Right now it would seem I should turn off the computer and go have a whiskey or two ... or three.

0
votes

You are registering in this context: kFHMemberDetailContext.
You are checking inequality for this context: kFHMemberDetailTableViewControllerContext.
Of course, these being constants, they might as well be the same.

Therefore, you will likely always call the super implementation. Perhaps that is what causes your additional notifications.

As a practical solution, check for the new key and simply return early if it is null.