Key-Value Coding is an Objective-C mechanism for introspection and (limited) Higher Order Messaging. If you don’t already know about it, you should read
- the documentation,
- NSHipster’s post about KVC Collection Operators,
- and “Let’s build Key-Value Coding” by Mike Ash.
While KVC is already really powerful, its implementation hides some really nice (and undocumented) details.
The Basics — KVC 101
There are two basic methods: valueForKey:
and setValue:forKey:
.
[foo valueForKey:@"bar"]
Provided foo
implements the -bar
method, this is the same as:
[foo bar]
It will fallback to other methods like getBar
and countOfBar
/objectInBarAtIndex:
, ultimately accessing the instance variable directly.1
When used on NSArrays and NSSets2, it calls valueForKey:
on each item, and returns a new Collection with the results.
[@[steveJobs, billGates] valueForKey:@"coolness"]
// -> @[@"A LOT.",@"Not that much."]
(According to Bill Gates. But I think Jobs never did this.)
The extended version of valueForKey:
is valueForKeyPath:
. It uses KVC Paths, sequence of keys separated by periods and evaluated from left to right.
[object valueForKeyPath:@"a.b.c"]
It’s the same as:
[[[object valueForKey:@"a"] valueForKey:@"b"] valueForKey:@"c"]
Of course, KVC Paths can also be used on collections:
[@[steveJobs, billGates] valueForKeyPath:@"car.brand"]
// -> @[@"Mercedes", @"Porsche"]
Calling All Methods
It’s important to realize that KVC predates Objective-C 2.0. This means that it doesn’t work just with @properties : you can “get the value” of any method with no parameter.
[@"hello" uppercaseString]
// -> @"HELLO"
[@"hello" valueForKey:@"uppercaseString"]
// -> @"HELLO"
Again, this works with collections too:
[@[@"hello", @"goodbye"] valueForKey:@"uppercaseString"]
// -> @[@"HELLO", @"GOODBYE"]
It’s really any method. For example, -[NSObject self]
is a method like any other.
[@"hello" valueForKey:@"self"]
// -> @"hello".
(This looks useless? See below.)
Collections Operators
Collection operators are special components of KVC paths, that start with an “@”. Officially, there are 10 of them:
The simple ones:
@"@max"
@"@min"
@"@avg"
@"@sum"
The slightly less trivial ones:
@"@distinctUnionOfObjects"
@"@unionOfObjects"
The ones for which I have to read the documentation everytime:
@"@unionOfArrays"
@"@distinctUnionOfArrays"
@"@distinctUnionOfSets"
And finally, the one that does not really belong here:
@"@count"
Let’s begin with this one. It just calls -[NSArray count]
.
[@[@"a",@"b"] count]
// -> 2
[@[@"a",@"b"] valueForKey:@"@count"]
// -> @(2)
It’s different from the others, because it really works on the Collection itself, and not its items. I don’t consider it to be a collection operator, really.
Additionally, it doesn’t need any special implementation:
In fact, any keypath component starting with an “@” will use a property of the collection object, instead of collecting properties of the collection’s items.
(Of course, this is undocumented behavior.3)
This means we can call any method of the Collection object:
[@[@"a",@"b"] valueForKey:@"@lastObject"]
// -> @"b"
Let’s add a -reverse
method to NSArray4:
@implementation NSArray (Reverse)
- (instancetype) reverse
{
return [[self reverseObjectEnumerator] allObjects];
}
@end
Behold! The custom @reverse
“operator”!
[array valueForKey:@"@reverse"]
// -> @[@"b",@"a"]
(By the way, note that this operator returns a Collection.)
Again, this is undocumented behavior. Don’t use it.
Collection Operators (for real)
OK, let’s look a little deeper at the other guys. How do they work? Here is the example in the official documentation:
[object valueForKeyPath:
@"<keypathToCollection>.<@collectionOperator>.<keypathToProperty>"]
This is the same as calling:
[[object valueForKeyPath:@"<keyPathToCollection>"]
valueForKeyPath:@"<@collectionOperator>.<keypathToProperty>"]
Step by step, that would be:
[[[object valueForKeyPath:@"<keyPathToCollection>"]
valueForKeyPath:@"<keypathToProperty>"]
<performTheCollectionOperator>]
// -> Fetch the objects for the right part of the keypath,
// then perform the operation
Let’s take a look for a moment at the first group: @max
, @min
, @avg
and @sum
. Apple calls them “Simple Collection Operators”.
[company valueForKeyPath:@"employees.@avg.salary"]
This is equivalent to:
[[company valueForKeyPath:@"employees.salary"] average]
(Except that the average
method doesn’t exist. Just pretend it exists, or write it.)
Here’s what the implementation for @avg
does:
- Call
[self valueForKeyPath:@"salary"]
The result must be a collection of NSNumbers. - Computes the average and return it as an NSNumber.
It’s important to note that the part to the right of @collectionOperator
is a parameter for the operator’s implementation.
The second group, @unionOfObjects
and @distinctUnionOfObjects
, work on a collection of objects. The documentation refers to these as “Object Operators”.
The first one, @unionOfObjects
, is basically useless.
NSArray* pencils = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"blue"},
@{@"color": @"green"}];
[pencils valueForKeyPath:@"@unionOfObjects.color"]
// -> @[@"blue", @"red", @"blue", @"green"]
It’s exactly the same as calling [pencils valueForKey:@"color"]
. I’m not sure why this operator even exists.
@distinctUnionOfObjects
works the same, but gets rid of duplicates:
[pencils valueForKeyPath:@"@distinctUnionOfObjects.color"]
// -> @[@"blue", @"red", @"green"]
Nice and easy.
The last group, @distinctUnionOfArrays
, @unionOfArrays
, @distinctUnionOfSets
work on a Collection of Collections. (In the docs: “Array and Set Operators”)
@unionOfArrays
does what it says:
NSArray* pencils = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"blue"}];
NSArray* markers = @[@{@"color": @"purple"},
@{@"color": @"blue"},
@{@"color": @"green"}];
[@[pencils, markers] valueForKeyPath:@"@unionOfArrays.color"]
// -> @[@"blue", @"red", @"blue", @"purple", @"blue", @"green"]
and @distinctUnionOfArrays
gets rid of duplicates5:
NSArray* pencils = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"blue"}];
NSArray* markers = @[@{@"color": @"purple"},
@{@"color": @"blue"},
@{@"color": @"green"}];
[@[pencils, markers] valueForKeyPath:@"@distinctUnionOfArrays.color"]
// -> @[@"green", @"red", @"blue", @"purple"]
@distinctUnionOfSets
works exactly like @distinctUnionOfArrays
, and there’s, of course, no @unionOfSets
, because, well, there can’t be duplicates in NSSets.
KVC + self = Love
As I said above, -[NSObject self]
is a method like any other.
You can of course use it in KVC Paths:
[numbers valueForKeyPath:@"@avg.self"]
// same as [[numbers valueForKeyPath:@"self"] average]
This calls “self” on each item, creates a new array with the results, and computes the average.
This is the easiest way to get the average (or sum, maximum, minimum) of an NSArray of NSNumbers.
If you were in the mood, you could even use @self
(Remember! Undocumented!):
[numbers valueForKeyPath:@"@avg.@self"]
// same as [[numbers valueForKeyPath:@"@self"] average]
This calls “self” on the array and computes the average.
Hidden Treasure: Custom Collections Operators
(Hat tip goes to Nicolas Bachschmidt for this discovery.)
Of course, this too is completely undocumented.
The valueForKeyPath:
method looks for the implementation of operators in method named like this:
- (id) _<operator>ForKeyPath:(NSString*)keyPath
NSArray implements _avgForKeyPath:
, _sumForKeyPath
, etc6.
You can create new methods on this template, and valueForKeyPath:
will pick them up.
For example, let’s add a @standardDeviation
operator:
- (NSNumber*) _standardDeviationForKeyPath:(NSString*)keyPath
{
[...]
}
[...]
[@[@0, @5, @5, @5, @5] valueForKeyPath:@"@standardDeviation.self"]
// -> @2
And the @differential
operator:
- (NSArray*) _differentialForKeyPath:(NSString*)keyPath
{
[...]
}
[...]
[@[@0, @1, @3, @0] valueForKeyPath:@"@differential.self"]
// -> @[@1, @2, @(-3)]
The return type is up to the operator: it can return an simple object or a collection.
Let’s build a better @distinctUnionOfObjects
There are two annoying limitations with the implementation of @distinctUnionOfObjects
and his friends. First, an important detail in the docs:
Important: This operator raises an exception if any of the leaf objects is nil.
Additionally, each of the items in the collection must be KVC-compliant for the key being collected.
Here’s the pencils example again:
id pencils = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"green"},
@"notacolor"]; // <- !
[pencils valueForKeyPath:@"@distinctUnionOfObjects.color"];
// -> NSUnknownKeyException: "This class is not key value coding-compliant for the key color"
It crashes. Let’s fix this.
We want a new operator similar to @unionOfObjects
, but with these additional properties:
- it ignores incompatible items,
- it ignores nil values,
- it ignores NSNull values.
Let’s name it @distinctUnionOfPresentObjects
. Here’s a quick implementation:
- (id) _distinctUnionOfPresentObjectsForKeyPath:(NSString*)keyPath
{
NSMutableArray * values = [NSMutableArray new];
for (id obj in self) {
@try {
id value = [obj valueForKeyPath:keyPath];
if(value && value!=[NSNull null] &&
![values containsObject:value])
{
[values addObject:value];
}
}
@catch (id) {}
}
return [NSArray arrayWithArray:values];
}
Then, in use:
id objects = @[@{@"color": @"blue"},
@{@"color": @"red"},
@{@"color": @"green"},
@"notacolor"];
[objects valueForKeyPath:@"@distinctUnionOfPresentObjects.color"] // -> @[@"blue", @"red", @"green"].
No crash!
Operators Galore
Let’s push it a little further. As said above, the part of the keypath to the right of an @-operator is a parameter to this operator’s implementation. We can use it for what we want.
For example, an image-resizing operator:
[images valueForKeyPath:@"@resizedTo.{128,128}"]
A regexp filter:
[useragents valueForKeyPath:@"@filterWithRegexp.[Mm]ozilla"]
Or a unit-conversion operator:
[arrayOfMiles valueForKeyPath:@"@convertFromTo.miles.KM"]
The possibilities are endless!
Wrap Up
KVC is already very powerful, and with Custom Operators, it would really be fantastic. Unfortunately, it’s all undocumented implementation details right now, and KVC is a little forgotten by Apple these days. As far as I can tell, it hasn’t changed at all since at least Mac OS X 10.4. I filed a bug requesting an API, I encourage you to duplicate it.
Comments are welcome, via nico@bou.io or on twitter.
##
Most of this post comes from a talk (in french) I gave at Cocoaheads Paris in december 2012.
-
In other words, there are virtually no private ivars in Objective-C. Here are the Apple docs for the implementation details. The default implementation of
+accessInstanceVariablesDirectly
returns YES (for compatibility reasons), and nobody ever overrides it. ↩ -
Using
valueForKey:
with an NSDictionary is basically the same as usingobjectForKey:
. As for NSOrderedSet, as of now, it doesn’t support KVC operators at all. ↩ -
It’s been like for a long time, actually. I think I read about it on a cocoadev discussion a few years ago. ↩
-
By the way, many people are doing it wrong. ↩
-
For some reason,
@distinctUnionOfArrays
loses the ordering. The documentation does not mention it, but in my testing, its sibling@distinctUnionOfObjects
did maintain relative ordering in the filtered keys. ↩ -
Actually, probably not NSArray : it’s a Class Cluster. Its concrete subclasses do. ↩