A note about KVO dependent keys
In my previous post I mentioned +keyPathsForValuesAffectingValueForKey:
and how you should always add the result from calling this method on super
to the set you return.
I also noted that using +keyPathsForValuesAffecting<Key>
could be a bad idea since it adds complexity when using dependencies across a class hierarchy.
Let’s see what I meant by that with an example (the example is available as a gist, probably easier to read)
@interface Animal : NSObject
- (NSString *)identity;
@property (copy, nonatomic) NSString *name;
@end
@implementation Animal
- (NSString *)identity
{
return [NSString stringWithFormat:@"My name is %@.", [self name]];
}
@end
@interface Monkey : Animal
@property (assign, nonatomic) NSUInteger age;
@end
@implementation Monkey
- (NSString *)identity
{
return [[super identity] stringByAppendingFormat:@" I am %lu.", [self age]];
}
@end
This is a simple example: an Animal
class declaring an identity
method and a name
property. The identity will change based on the name. And another Monkey
class, a subclass of Animal
, adding an age
property and overriding identity
to include the age
.
Now, given that the value returned by identity
will differ based on the values of the name
and age
properties we will want to let KVO know about these dependencies so that an observer is dully notified when such value changes.
Returning key paths which values affect the value of a key
The NSKeyValueObservingCustomization
informal protocol declares a method that lets us do just this: +keyPathsForValuesAffectingValueForKey:
.
Alternatively, you can implement a shortcut method +keyPathsForValuesAffecting<Key>
where <Key>
is the key being affected. I usually don’t recommend using this method and I will explain why.
But first, let’s see how one would implement +keyPathsForValuesAffectingValueForKey:
for our example.
+keyPathsForValuesAffectingValueForKey:
Using The docs clearly specify how +keyPathsForValuesAffectingValueForKey:
works:
The default implementation of this method searches the receiving class for a method whose name matches the pattern
+keyPathsForValuesAffecting<Key>
, and returns the result of invoking that method if it is found. Any such method must return an NSSet. If no such method is found, an NSSet that is computed from information provided by previous invocations of the now-deprecatedsetKeys:triggerChangeNotificationsForDependentKey:
method is returned, for backward binary compatibility.
A key point is that the default implementation (leaving the backward compatibility note aside) will return an NSSet
. In other words, it is always safe to call super
, even if the superclass is not directly implementing this method.
Keeping this in mind, a natural implementation in both Animal
and Monkey
will be as following:
// In Animal
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSMutableSet *keyPaths = [NSMutableSet setWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
if ([key isEqualToString:@"identity"]) {
[keyPaths addObject:@"name"];
}
return keyPaths;
}
// In Monkey
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key
{
NSMutableSet *keyPaths = [NSMutableSet setWithSet:[super keyPathsForValuesAffectingValueForKey:key]];
if ([key isEqualToString:@"identity"]) {
[keyPaths addObject:@"age"];
}
return keyPaths;
}
As explained in the previous post, it is very important to aggregate the key paths in Monkey
with the ones returned by the superclass Animal
. Since the subclass is overriding this method, if it lacked doing so the identity
key wouldn’t be seen as affected by the name
key path by KVO.
+keyPathsForValuesAffecting<Key>
Using People hating the fact that KVO funnels most of its work through a couple of methods might be lured into using +keyPathsForValuesAffecting<Key>
. These same people might also end up implementing these methods as following:
// In Animal
+ (NSSet *)keyPathsForValuesAffectingIdentity
{
return [NSSet setWithObject:@"name"];
}
// In Monkey
+ (NSSet *)keyPathsForValuesAffectingIdentity
{
return [NSSet setWithObject:@"age"];
}
You probably can spot the problem here in 2 seconds: the Monkey
implementation overrides the Animal
one, discarding anything it returns.
We need to, somehow, retrieve the key paths from the Animal
implementation and aggregate to the key paths we return in Monkey
.
But here comes the problem: which method should we call on super
? We need to assume that the implementation of this method is not exposed by the superclass. Even if it was declared, can we safely assume that it’s correctly implemented by the superclass? Remember that +keyPathsForValuesAffecting<Key>
will not end up calling +keyPathsForValuesAffectingValueForKey:
on your behalf but is rather the other way around.
// In Monkey
+ (NSSet *)keyPathsForValuesAffectingIdentity
{
/*
This will not compile if `Animal` doesn't declare that it implements
`+keyPathsForValuesAffectingIdentity`.
If it did declare it but didn't implement, KVO would not end up
calling `+keyPathsForValuesAffectingValueForKey:` for us and an
unrecognized selector exception would be thrown at runtime.
*/
NSMutableSet *keyPaths = [NSMutableSet setWithSet:[super keyPathsForValuesAffectingIdentity]];
[keyPaths addObject:@"age"];
return keyPaths;
}
I cannot think of a nice way to fix this problem other than always declaring the
+keyPathsForValuesAffecting<Key>
methods that you implement so that subclasses can safely call super
. This is not very nice.
A workaround would be to call +keyPathsForValuesAffectingValueForKey:@"<Key>"
on super
.
// In Monkey
+ (NSSet *)keyPathsForValuesAffectingIdentity
{
NSMutableSet *keyPaths = [NSMutableSet setWithSet:[super keyPathsForValuesAffectingValueForKey:@"identity"]];
[keyPaths addObject:@"age"];
return keyPaths;
}
However, if you ended up doing this, I’d argue that you could probably forgo using +keyPathsForValuesAffecting<Key>
altogether and simply implement +keyPathsForValuesAffectingValueForKey:
in the subclass.
+keyPathsForValuesAffecting<Key>
?
When should I use As we’ve seen, it’s never really appropriate to +keyPathsForValuesAffecting<Key>
. There is a very obvious case for it though. Can you think about it? Let me try and lead you to it.
Can you think of a case where you wouldn’t want to override a method? Maybe because you wouldn’t know how to call this method on super
? Maybe because you might end up stomping its implementation? Yes, categories!
Dependent keys in a category
Once again, the docs for +keyPathsForValuesAffectingValueForKey:
give you quite a hint:
Note You must not override this method when you add a computed property to an existing class using a category, overriding methods in categories is unsupported. In that case, implement a matching
+keyPathsForValuesAffecting<Key>
to take advantage of this mechanism.
Say we created a category on Animal
to add a new method funkyName
. This method will simply prepend "Funky "
to the animal name. Obviously, the value returned by this method will depend on the value returned by the name
method.
In order for observers of funkyName
to be notified when the name changes, we will want to mark the name
key path as affecting the funkyName
key. Being in a category, we wouldn’t think of “overriding” the +keyPathsForValuesAffectingValueForKey:
method. It is indeed the perfect time to use +keyPathsForValuesAffecting<Key>
!
@interface Animal (Funky)
@property (readonly, copy, nonatomic) NSString *funkyName;
@end
@implementation Animal (Funky)
- (NSString *)funkyName
{
return [@"Funky " stringByAppendingString:[self name]];
}
+ (NSSet *)keyPathsForValuesAffectingFunkyName
{
return [NSSet setWithObjects:@"name", nil];
}
@end
The implementation of +keyPathsForValuesAffectingFunkyName
here doesn’t need to try and aggregate the result with the one from the class itself or its superclass. In fact, it’s nicely isolated in the category, along with the dependent property.
I hope this helped you understand how the key dependency KVO mechanism works. As I said in the previous post, KVO has very simple rules and is very powerful, as long as you follow the rules.
I have to credit my friend and former colleague Keith Duncan. He's the KVO guru. I have merely been trying to absorb as much knowledge from him as I could while we were working together. True story.