Lightweight Key-Value Observing
Making KVO simpler and easier
In this article, I’d like to implement a simple class I use for key-value observing. I think KVO is great, however, for most of what I do, there are two problems:
-
I don’t like the dispatching in
observeValueForKeyPath:ofObject:change:context:
. I think it gets messy and confusing if you observe more than one object. -
You have to balance each add observer with a remove observer , it would be nice if this can be done automatically.
So, off we go. The trick we will use is one I first saw inTHObserversAndBinders, and this post is basically a description of what they did, but in the most minimalistic way.
First, we’ll define the interface for our object:
@interface Observer : NSObject
+ (instancetype)observerWithObject:(id)object
keyPath:(NSString*)keyPath
target:(id)target
selector:(SEL)selector;
@end
The observer takes four parameters, which are hopefully self-explanatory. I chose to use the target/action pattern: an alternative would have been blocks, but then you would have to do the weakSelf/strongSelf dance, and it’s often nice to have a separate method anyway.
What we will do is set up the KVO inside the initializer, and remove it in thedealloc
method. What this means is that as long as the Observer
object is
retained, we will have an observer. The way I typically use this, for example,
in a view controller:
self.usernameObserver = [Observer observerWithObject:self.user
keyPath:@"name"
target:self
selector:@selector(usernameChanged)];
By putting it in a property, we are making sure it gets retained. As soon as our view controller deallocates, it’ll set the property to nil and the observer will stop observing.
In the implementation, it’s important that we keep a weak reference to both the observed object and the target. If one of the two gets nil, we want to stop sending messages:
@interface Observer ()
@property (nonatomic, weak) id target;
@property (nonatomic) SEL selector;
@property (nonatomic, weak) id observedObject;
@property (nonatomic, copy) NSString* keyPath;
@end
The initializer sets up the KVO notifications. It uses self
as the context.
This is necessary if we would ever have a subclass that adds a similar
observer:
- (id)initWithObject:(id)object keyPath:(NSString*)keyPath target:(id)target selector:(SEL)selector
{
if (self) {
self.target = target;
self.selector = selector;
self.observedObject = object;
self.keyPath = keyPath;
[object addObserver:self forKeyPath:keyPath options:0 context:self];
}
return self;
}
When a change happens, we just notify our target, if it still exists:
- (void)observeValueForKeyPath:(NSString*)keyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context
{
if (context == self) {
id strongTarget = self.target;
if ([strongTarget respondsToSelector:self.selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[strongTarget performSelector:self.selector];
#pragma clang diagnostic pop
}
}
}
And finally, in the dealloc
we remove the observer:
- (void)dealloc
{
id strongObservedObject = self.observedObject;
if (strongObservedObject) {
[strongObservedObject removeObserver:self forKeyPath:self.keyPath];
}
}
That’s all there is to it. There are a lot of ways this could be extended: add blocks support, or my favorite trick: another convenience constructor that call the action directly the first time. However, I wanted to show the core of the technique, adjust it to your needs.
By using this technique you don’t have to remember too much when doing KVO. Just retain the observers, and set them to nil when you’re done. The rest will happen automatically.