KVO – Observing any change in Object state

I have a situation where it would be really handy to bind an entire object (and its encapsulated states) to a view.  As this view displays itself depending on the all the different values of its member variables, it could be very tedious to set all this up, property by property, when all a view object really needs to know is when its underlying data model has changed and it should therefore redraw itself.

So, I thought I would post my solution to the problem.  Basically the approach is to create a property that you can observe (call it observableSelf), and will send change notifications any time any other property of that object changes.  Other than the fact that you have to observe some extra property, I find the solution kind of elegant.

meh.  Here’s the code.  It speaks for itself.

//
//  KVOUpdatingObject.h
//
//  Created by Stephen O'Connor on 1/28/13.
//  Copyright (c) 2013 Stephen O'Connor. All rights reserved.

#import <Foundation/Foundation.h>

typedef NSUInteger ChangeMonitor;  // I put this here because it seemed better to
                                   //define an appropriate type than int, although it doesn't really matter.

@interface KVObservableObject : NSObject

@property (nonatomic, assign) ChangeMonitor observableSelf; // basically you observe this

@end

and now the .m

//  KVOUpdatingObject.m
//  CocoaBindingsTesting
//
//  Created by Stephen O'Connor on 1/28/13.
//  Copyright (c) 2013 Stephen O'Connor. All rights reserved.
//

#import "KVOUpdatingObject.h"
#import <objc/runtime.h> // very important to import this!

@implementation KVOUpdatingObject

+(NSSet*)keyPathsForValuesAffectingValueForKey:(NSString*)key {

    NSSet *keyPaths = [super keyPathsForValuesAffectingValueForKey:key];

    if ([key isEqualToString:@"observableSelf"]) {
        NSMutableArray *keys = [NSMutableArray array];
        unsigned int count;
        objc_property_t *properties = class_copyPropertyList([self class], &count);  // see imports above!
        for (size_t i = 0; i < count; ++i) {
            NSString *property = [NSString stringWithCString:property_getName(properties[i])
                                                    encoding:NSASCIIStringEncoding];

            if ([property isEqualToString: @"observableSelf"] == NO) {
                [keys addObject: property];
            }
        }
        free(properties);

        keyPaths = [keyPaths setByAddingObjectsFromArray: keys];
    }

    return keyPaths;
}

@end

KVC is easy to understand. KVO starts to get a little more complicated, and Cocoa Bindings is the next step up. I’ve been having trouble with Bindings and CoreData only because I haven’t had very much time with it. It’s amazing what Apple hides from the Developer and yet still enables the Developer to do wonderful things. But slowly the Developer needs to know more and more and go deeper. I’m still on my way down that road.

This is easily tested to give you some safe feeling. I created a subclass of the above, gave it a NSNumber property and an NSArray property (someNumber, and someObjects respectively). Here’s the test case:

@implementation CocoaBindingsTestingTests

- (void)setUp
{
    [super setUp];

    // Set-up code here.
    _testObject = [KVOTesting new];   // the subclass of KVObservableObject

    _observationCount = 0;  // some instance variable on this test case

    [_testObject addObserver:self forKeyPath:@"observableSelf"
                     options:NSKeyValueObservingOptionNew
                     context:nil];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"observableSelf"]) {

        NSLog(@"Object Changed!");
        _observationCount++;
    }
    else
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}

- (void)tearDown
{
    // Tear-down code here.
    [super tearDown];
}

- (void)testExample
{
    [_testObject setValue:@4 forKey:@"someNumber"];  
    _testObject.someNumber = @4; 
    _testObject.someObjects = @[@2, @3, @4]; 
    [_testObject setSomeNumber: @5];

    STAssertTrue(_observationCount == 4, @"Should have reported 4 changes");
}

@end

Now, the only remaining issue for such a use case is what if this object is a member of another object, and this object's properties change, but not the object itself.  How would that 'parent' know?

Advertisements