Sunday, August 23, 2009

Coolest thing I learnt at WWDC 2009

Cocoa's support for KVO and Properties is awesome, but every now and then I find something I can't do. I frequently use 'derived' properties, where a read only property is derived from a combination of instance variables. For example, we might have an object with firstName and lastName, then want a KVO compliant fullName.

Cocoa did offer some support for this in your own classes by doing a willChangeValueForKey: and didChangeValueForKey:. The real challenge comes when you want to have the derived accessor in a class category in the app layer where the base class is in a framework. The framework base class has no knowledge of class extension in the app layer, so can't post the willChange/didChange, so your derived accessor is not KVO compliant.

Let's look at how things used to work with a basic example using firstName, lastName, fullName in our own object.

Header...

#import


@interface Person : NSObject {

NSString *firstName;

NSString *lastName;

}


- (void)setFirstName:(NSString *)value;

- (NSString *)firstName;


- (void)setLastName:(NSString *)value;

- (NSString *)lastName;


- (NSString *)fullName;


@end



Implementation....

#import "Person.h"


@implementation Person


- (void)dealloc{

[firstName release], firstName = nil;

[lastName release], lastName = nil;

[super dealloc];

}


- (void)setFirstName:(NSString *)value{

if ([firstName isEqualToString:value]) return;

[self willChangeValueForKey:@"fullName"];

[firstName release];

firstName = [value retain];

[self didChangeValueForKey:@"fullName"];

}


- (NSString *)firstName{

return [[firstName retain] autorelease];

}


- (void)setLastName:(NSString *)value{

if ([lastName isEqualToString:value]) return;

[self willChangeValueForKey:@"fullName"];

[lastName release];

lastName = [value retain];

[self didChangeValueForKey:@"fullName"];

}


- (NSString *)lastName{

return [[lastName retain] autorelease];

}


- (NSString *)fullName{

return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];

}


@end



I presented this quandary to an Apple engineer at WWDC 09 and after a few minutes he found this in NSKeyValueObserving.h and became my new hero. (I love WWDC for just this reason... the guy saved me an unbelievable amount of work).

/* Return a set of key paths for properties whose values affect the value of the keyed property. When an observer for the key is registered with an instance of the receiving class, KVO itself automatically observes all of the key paths for the same instance, and sends change notifications for the key to the observer when the value for any of those key paths changes. The default implementation of this method searches the receiving class for a method whose name matches the pattern +keyPathsForValuesAffecting, and returns the result of invoking that method if it is found. So, any such method must return an NSSet too. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecated +setKeys:triggerChangeNotificationsForDependentKey: method is returned, for backward binary compatibility.


This method and KVO's automatic use of it comprise a dependency mechanism that you can use instead of sending -willChangeValueForKey:/-didChangeValueForKey: messages for dependent, computed, properties.

You can override this method when the getter method of one of your properties computes a value to return using the values of other properties, including those that are located by key paths. Your override should typically invoke super and return a set that includes any members in the set that result from doing that (so as not to interfere with overrides of this method in superclasses).


You can't really override this method when you add a computed property to an existing class using a category, because you're not supposed to override methods in categories. In that case, implement a matching +keyPathsForValuesAffecting to take advantage of this mechanism.

*/


In a nut shell, all you have to do is add a class method and return the key paths for the value that affect your derived accessor....

+ (NSSet *)keyPathsForValuesAffectingFullName{

return [NSSet setWithObjects:@"firstName",@"lastName"];

}



So the implementation can be simplified to this...

#import "Person.h"


@implementation Person


@synthesize firstName, lastName;


- (void)dealloc{

[firstName release], firstName = nil;

[lastName release], lastName = nil;

[super dealloc];

}


- (NSString *)fullName{

return [NSString stringWithFormat:@"%@ %@",self.firstName,self.lastName];

}


+ (NSSet *)keyPathsForValuesAffectingFullName{

return [NSSet setWithObjects:@"firstName",@"lastName"];

}


@end



And the header this....

#import


@interface Person : NSObject {

NSString *firstName;

NSString *lastName;

}


@property (retain) NSString *firstName;

@property (retain) NSString *lastName;

@property (readonly) NSString *fullName;


@end



The moral:
- Much less code
- Can do it in a class category

Only downside is 10.5+ only, but you shouldn't be worried about 10.4 by now right :-)