15
Apr
2008
 

Cocoa Tutorial: Get The Most Out of Key Value Coding and Observing

by Marcus Zarra

Key Value Observing/Key Value Coding (KVO/KVC) is probably one of the most powerful and most under-utilized features of Objective-C. Here are a couple of examples of how to get the most out of it

When a call is made on an object through Key Value Coding such as [self valueForKey:@”someKey”], numerous attempts are made to resolve the call. First, the method someKey is looked for. If that is not found then the iVar someKey is looked for. If neither of those are found, then one last attempt is made before presenting an error. That last attempt is a call to the method -(id)valueForUndefinedKey:. If that method is not implemented then an NSUndefinedKeyException is raised.

valueForUndefinedKey: is designed so that when you request a value from an object using -(id)valueForKey: the object has a last chance to respond to that request before an error occurs. This has many benefits and I have included two examples of those benefits in this post–Core Data Parameters and Data Formatting.

Core Data Parameters

One of the ways in which I use this method is by making a singleton object that responds to parameter requests. This allows any part of the application to ask the singleton for parameters and get them back in a clean block of code rather than having to build an NSFetchRequest each time a parameter is needed. This allows others parts of the application to make simple calls like:

NSString *companyName = [[Company shared] valueForKey:kCompanyName];

and avoid messy calls like this:

NSString *companyName = nil;

NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];

[request setEntity:[NSEntityDescription entityForName:@"Parameter" inManagedObjectContext:managedObjectContext]]];
[request setPredicate:[NSPredicate predicateWithFormat:@"name == %@", kCompanyName]];

NSError *error = nil;
companyName = [[managedObjectContext executeFetchRequest:request error:&error] lastObject];
NSAssert(error == nil, ([NSString stringWithFormat:@"Error requesting parameter: %@\n%@", kCompanyName, error]));

Worse is when I want to set a parameter value:

id parameter;

NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
[request setEntity:[NSEntityDescription entityForName:@"Parameter" inManagedObjectContext:managedObjectContext]];
[request setPredicate:[NSPredicate predicateWithFormat:@"name == %@", key]];
 
parameter = [[managedObjectContext executeFetchRequest:request error:&error] lastObject];
NSAssert(error == nil, ([NSString stringWithFormat:@"Error getting parameter %@:%@", key, error]));
if (!parameter) {
    parameter = [NSEntityDescription insertNewObjectForEntityForName:@"Parameter" inManagedObjectContext:managedObjectContext];
    [parameter setValue:key forKey:@"name"];
}

if ([value isKindOfClass:[NSNumber class]]) {
    [parameter setValue:[value stringValue] forKey:@"value"];
} else if ([value isKindOfClass:[NSDate class]]) {
    [parameter setValue:[value description] forKey:@"value"];
} else {
    [parameter setValue:value forKey:@"value"];
}

As you can see, after the 5th or 6th time of plugging that block of code in just to get a parameter out of the Core Data store — it gets old. Therefore, I turned to KVC/KVO.

Not: These parameters are different from using NSUserDefaults. NSUserDefaults are great for application level parameters. However, if you need to store document level parameters then NSUserDefaults is the wrong place.

Instead of writing tons of code to access the Core Data Stack, parameter access can be centralized in a singleton. That singleton can then be exposed to the entire application and any portion of the application can then request or set parameters. The parameters are then read and set via the valueForUndefinedKey: and setValue:forUndefinedKey: methods as shown:

- (id)valueForUndefinedKey:(NSString *)key;
{
    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
    [request setEntity:[NSEntityDescription entityForName:@"Parameter" inManagedObjectContext:managedObjectContext]]];
    [request setPredicate:[NSPredicate predicateWithFormat:@"name == %@", key]];

    NSError *error = nil;

    NSManagedObject *parameter = [[managedObjectContext executeFetchRequest:request error:&error] lastObject];
    NSAssert(error == nil, ([NSString stringWithFormat:@"Error requesting parameter: %@\n%@", key, error]));
    return [parameter valueForKey:@"value"];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key 
{
    [self willChangeValueForKey:key];

    NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];

    [request setEntity:[NSEntityDescription entityForName:@"Parameter" inManagedObjectContext:managedObjectContext]];
    [request setPredicate:[NSPredicate predicateWithFormat:@"name == %@", key]];
    NSError *error = nil;

    NSManagedObject *parameter = [[managedObjectContext executeFetchRequest:request error:&error] lastObject];
    NSAssert(error == nil, ([NSString stringWithFormat:@"Error getting parameter %@:%@", key, error])];
    if (!parameter) {
        parameter = [NSEntityDescription insertNewObjectForEntityForName:@"Parameter" inManagedObjectContext:managedObjectContext];
        [parameter setValue:key forKey:@"name"];
    }
    if ([value isKindOfClass:[NSNumber class]]) {
        [parameter setValue:[value stringValue] forKey:@"value"];
    } else if ([value isKindOfClass:[NSDate class]]) {
        [parameter setValue:[value description] forKey:@"value"];
    } else {
        [parameter setValue:value forKey:@"value"];
    }
    [self didChangeValueForKey:key];
}

As you can see this code is pretty much identical to the blocks listed above. However, now it is only in one place in the code and not littered around the entire application. In addition, if I ever decide to move the location where parameters are stored, I only have to alter that code once. By using the valueForUndefinedKey: method, I can avoid having to alter the code every time I want to store another parameter. Lastly, since the parameter key is considered a value on the singleton object, I can observe it and receive notifications when it changes. This allows me to hook IB bindings directly to the singleton.

Date Formatting

While I do not have an axe to grind against dates in Cocoa, in my opinion the formatters could use some work. Localizing dates directly in Interface Builder (along with currency formatters) is a hit or miss situation. For the display of dates, I simply gave up. I can get the localizers to work just fine in code and I can manually set the formatters for display but there is an easier way to do it.

As part of my modified Core Data Stack, I have a parent class that subclasses NSManagedObject. All of my objects then extend this subclass. This (super/sub)class allows me to load in some interesting things such as overriding the valueForUndefinedKey: method. In this case, however, I am doing some trickery on the key itself.

As an example, most of my data objects have a value called ‘createDate’ that is set in the awakeFromInsert method. This attribute is stored in Core Data as an NSDate. To get around the issues with the date formatters in InterfaceBuilder, I overrode the valueForUndefinedKey: method in the ZDSManagedObject as follows:

static NSString* const kZDSLocalDateStringSuffix = @"LocalString";
static NSString* const kZDSLocalDateTimeStringSuffix = @"LocalDTString";

- (id)valueForUndefinedKey:(NSString *)key
{
    if ([key hasSuffix:kZDSLocalDateStringSuffix]) {
        id value = [self valueForKey:[key substringToIndex:[key rangeOfString:kZDSLocalDateStringSuffix].location]];
        if (!value) return [super valueForUndefinedKey:key];
        if ([value isKindOfClass:[NSDate class]]) {
            return [[AppDelegate shortDateFormatter] stringFromDate:value];
        }
        return [super valueForUndefinedKey:key];
    }
    if ([key hasSuffix:kZDSLocalDateTimeStringSuffix]) {
        id value = [self valueForKey:[key substringToIndex:[key rangeOfString:kZDSLocalDateTimeStringSuffix].location]];
        if (!value) [super valueForUndefinedKey:key];
        if ([value isKindOfClass:[NSDate class]]) {
            return [[AppDelegate shortDateTimeFormatter] stringFromDate:value];
        }
        return [super valueForUndefinedKey:key];
    }
    return [super valueForUndefinedKey:key];
}

With this code in place, I can set the createDate binding in Interface Builder to createDateLocalString and the field will be populated with the create date formatted properly for the locale. I can also set the binding to createDateLocalDTString and receive the date and time formatted properly for the locale.

Conclusion

There is a lot more than can be done with Key Value Coding and Observing and the undefinedKey methods. These two examples are just a start of what can be done to easily and cleanly roll up code that is reused frequently.

Comments

joo_p23 says:

may i suggest using a wordpress template that is wider so that you don’t have to scroll horizontally on the code examples.