Much can be said about key–value observing (KVO). At very least, it’s an interesting use of Objective-C’s runtime dynamism. The Apple-provided API is rudimentary; I like to layer something on top that calls a block instead of a method that needs its own dispatch table. Other libraries like ReactiveCocoa wrap it in its own conventions.
But that higher-level stuff is a discussion for another time. Right now I want to talk about key path dependencies.
Key–value observing supports notifications both directly and indirectly.
Direct notifications, via [self willChangeValueForKey:@"foo"]
and [self didChangeValueForKey:@"foo"]
, are automatically wrapped around the corresponding -setFoo:
setter implementation method (by default).
But some properties don’t have direct setters. The canonical1 example for this is this class:
@interface Person : NSObject
@property (copy) NSString *givenName;
@property (copy) NSString *familyName;
@property (readonly) NSString *fullName;
@end
@implementation Person
- (NSString *)fullName {
return [@[self.givenName, self.familyName] componentsJoinedByString:@" "];
}
@end
The fullName
getter depends on givenName
and familyName
.
As such it should be annotated as depending on those key paths, so that when an object registers to observe fullName
it will get a notification when -setGivenName:
and -setFamilyName:
are called.
The +keyPathsForValuesAffectingValueForKey: method marks that dependency. The usual way to specify the above dependency is to implement a method with a special naming convention:
@implementation Person
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"givenName", @"familyName", nil];
}
- (NSString *)fullName { … }
@end
The default implementation of +keyPathsForValuesAffectingValueForKey:
dispatches to the method with this name (if any), just as -valueForKey:@"givenName"
dispatches to -givenName
and -setValue: forKey:@"givenName"
dispatches to -setGivenName:
.
Dependency helpers
A common case that comes up is:
@implementation SomeSingleton
- (BOOL)mySetting {
return [[NSUserDefaults standardUserDefaults] boolForKey:@"someKey"];
}
@end
If user defaults can change separately from this code (and you should probably assume it can), providing correct KVO change notifications can be accomplished by having the singleton observe NSUserDefaults
with NSKeyValueObservingOptionPrior
; posting willChangeValueForKey:@"mySetting"
on the prior callback and didChangeValueForKey:@"mySetting"
on the post callback.
But this sucks — it’s wordy and you pay a (small) performance cost even if mySetting
isn’t observed.
A simpler and more efficient approach is this:
@implementation SomeSingleton
+ (NSSet *)keyPathsForValuesAffectingMySetting {
return [NSSet setWithObject:@"$defaults.someKey"];
}
- (BOOL)mySetting { … }
- (NSUserDefaults *)$defaults {
return [NSUserDefaults standardUserDefaults];
}
@end
MyLilKeyPathHelpers adds this shortcut and a couple of others as a category on NSObject
2:
$defaults
for[NSUserDefaults standardUserDefaults]
$app
for the global sharedNSApplication
orUIApplication
instance (handy as$app.delegate.someKey
)$classes
as a generic method for the above: for example,$classes.SomeSingleton.sharedInstance.someKey
These make it easier to get properties KVO-compliant.
And yes, it’s legal to use $
in identifiers, but be very restrained with it!
Specifying dependencies
Instead of implementing the dependency method with a special name, it can be convenient to override the top-level method directly. For example, each item in Delicious Library has properties for fields like title, author, LCCN3, and notes. Sorting by these fields sometimes requires custom behavior — for example, sorting by author attempts to massage “Malcolm Gladwell” into “Gladwell, Malcolm”.
The way we implement this is to append “ForSorting” to each sort descriptor key.
Then we can provide an -authorForSorting
method with this custom behavior.
We also override -valueForKey:
to strip “ForSorting” if the object didn’t have a custom implementation, and just return the plain value.
NSArrayController
observes the keys of its sort descriptors to rearrange (re-sort) when one of the values change.
Accordingly, the “ForSorting” keys needed to be KVO-compliant.
Just as we override the dispatching -valueForKey:
method, we also overrode +keyPathsForValuesAffectingValueForKey:
to declare (by default)4 that authorForSorting
depends on author
.
Hell is other people’s superclasses
SICP made me a convert to the idea of composability.
When building a component on top of another — in this case, writing a class by subclassing NSObjet
— ideally you can build use on top of it in the same manner the original was constructed.
That is, your class should behave correctly when used as a superclass.
Let’s see what happens when we extend behavior of our original Person
class by subclassing.
The FancyPerson
class provides behavior for “John Smith, Esquire”, and TitledPerson
provides “Mr Smith”.
@interface FancyPerson : Person
@property (copy) NSString *suffix;
@end
@implementation FancyPerson
- (NSString *)fullName {
return [@[[super fullName], self.suffix] componentsJoinedByString:@", "];
}
@end
@interface TitledPerson : Person
@property (copy) NSString *title;
@end
@implementation TitledPerson
- (NSString *)fullName {
return [@[self.title, self.familyName] componentsJoinedByString:@" "];
}
@end
Both of these demonstrate a different behavior:
FancyPerson
enhances the fullName
method, by calling super and supplementing the return value.
TitledPerson
replaces the fullName
method, with no call to [super fullName]
.
Accordingly, -[FancyPerson fullName]
should declare that it depends on whatever -[Person fullName]
depends on, plus the suffix
key.
And -[TitledPerson fullName]
depends on only title
and familyName
, regardless of the superclass.
Let’s start with the enhancement case.
Enhancing key path dependencies
The naive approach has a compile error:
@implementation FancyPerson
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [[super keyPathsForValuesAffectingFullName] setByAddingObject:@"suffix"];
// error: no known class method for selector 'keyPathsForValuesAffectingFullName'
}
@end
Key path dependencies are typically private, not declared in a class’s interface, so the call to super will be a warning or error.
You could declare that you know the superclass to implement that method, but that couples to the implementation and may not be correct if Person
is instead overriding the dispatching method +keyPathsForValuesAffectingValueForKey:
.
Similar to subclassing delegates, the correct solution is to use the defining class5:
@implementation FancyPerson
+ (NSSet *)keyPathsForValuesAffectingFullName {
NSSet *superclassKeys = [[FancyPerson superclass] keyPathsForValuesAffectingValueForKey:@"fullName"];
return [superclassKeys setByAddingObject:@"suffix"];
}
@end
This is robust against the superclass’s implementation, no matter if it specifies dependencies by implementing the specific method, overriding the dispatching one, or none at all.
Replacing key path dependencies
Now let’s consider TitledPerson
’s override:
@implementation TitledPerson
+ (NSSet *)keyPathsForValuesAffectingFullName {
return [NSSet setWithObjects:@"title", @"familyName", nil];
}
@end
This is fine.
However there will be a problem if Person
were to instead override the dispatching method:
@implementation Person
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
if ([key isEqual:@"fullName"]) { // don't do this
return [NSSet setWithObjects:@"givenName", @"familyName", nil];
} else {
return [super keyPathsForValuesAffectingValueForKey:key];
}
}
In this case, the call [TitledPerson keyPathsForValuesAffectingValueForKey:@"fullName"]
completely ignores the TitledPerson
override!
Specifically, only when the call to super hits NSObject
’s implementation of +keyPathsForValuesAffectingValueForKey:
will the subclass’s override of +keyPathsForValuesAffectingFullName
be called.
You are in a maze of twisty little passages, all alike.
The problem here is the naive implementation of the +[Person keyPathsForValuesAffectingValueForKey:]
override.
Correctly overriding +keyPathsForValuesAffectingValueForKey:
It’s valid for dependencies to be specified in either the dispatching or specific methods, all the way up the inheritance chain.
Correctly overriding +keyPathsForValuesAffectingValueForKey:
requires some tricky code, which I’ve wrapped in a function MLHOverrideKeyPathsForValueAffectingKey in MyLilKeyPathHelpers.
The Person
class can use this to correctly override the dispatching method:
@implementation Person
+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key {
return MLHOverrideKeyPathsForValueAffectingKey(self, [Person class], NO, key, ^(NSSet *superKeyPaths) {
if ([key isEqual:@"fullName"]) { // or -hasSuffix:, etc.
return [NSSet setWithObjects:@"givenName", @"familyName", nil];
} else {
return superKeyPaths;
}
});
}
@end
With this implementation, calling [TitledPerson keyPathsForValuesAffectingValueForKey:@"fullName"]
results in a call to [TitledPerson keyPathsForValuesAffectingFullName]
and that’s all.
Accomplishing this unfortunately requires a reimplementation of the method name logic in NSObject
’s +keyPathsForValuesAffectingValueForKey:
; I believe this is inescapable.
All the parameters to this function are documented in the header; take a look.
This same technique could be applied to an override of -valueForKey:
, though that generally doesn’t have the same issues because the dispatch targets (that is, getter methods) are declared in a class’s public interface.
-
and ridden with Anglo-centric cultural assumptions; see Falsehoods Programmers Believe About Names ↩
-
As a rule I try to avoid categories on framework classes as much as possible. I feel this is one of the rare cases when it’s appropriate. ↩
-
Some “ForSorting” values depend on more than one key. For example,
-titleForSorting
depends on bothtitle
anddominantLanguageCode
, so “Die Another Day” sorts under “D” while your German copy of “Die Bourne Identität” sorts under “B”. Incidentally this causes a lot of user confusion when the language information is not correct. ↩ -
MyLilKeyPathHelpers provides a helper macro for this called
_definingClass
: ↩