Skip to content

Instantly share code, notes, and snippets.

@paulrehkugler
Created March 9, 2014 04:13
Show Gist options
  • Save paulrehkugler/9442834 to your computer and use it in GitHub Desktop.
Save paulrehkugler/9442834 to your computer and use it in GitHub Desktop.
Trying to make KVO improvements based on http://nshipster.com/key-value-observing/
//
// NSObject+PRKeyValueObserving.h
// PRKeyValueObserving
//
// Created by Paul Rehkugler on 3/8/14.
// Copyright (c) 2014 Paul Rehkugler. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void (^PRValueObservationHandler)(NSDictionary *change);
@interface NSObject (PRKeyValueObserving)
- (void) addValueObservationHandler:(PRValueObservationHandler)valueObservationHandler forSelector:(SEL)selector;
- (void) addValueObservationHandler:(PRValueObservationHandler)valueObservationHandler forSelector:(SEL)selector options:(NSKeyValueObservingOptions)options;
- (void) removeObservationHandlerForSelector:(SEL)selector;
@end
//
// NSObject+PRKeyValueObserving.m
// PRKeyValueObserving
//
// Created by Paul Rehkugler on 3/8/14.
// Copyright (c) 2014 Paul Rehkugler. All rights reserved.
//
#import "NSObject+PRKeyValueObserving.h"
@interface NSObject (PRKeyValueObserving_Internal)
- (void) pr_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
- (void) pr_safelyRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath;
@property (strong) NSArray *pr_valueObservationHandlers;
@end
@implementation NSObject (PRKeyValueObserving)
- (void) addValueObservationHandler:(PRValueObservationHandler)valueObservationHandler forSelector:(SEL)selector
{
[self addValueObservationHandler:valueObservationHandler forSelector:selector options:0];
}
- (void) addValueObservationHandler:(PRValueObservationHandler)valueObservationHandler forSelector:(SEL)selector options:(NSKeyValueObservingOptions)options
{
// TODO: I _really_ don't like casting to (void *) here
[self addObserver:self forKeyPath:NSStringFromSelector(selector) options:options context:(void *)[self.pr_valueObservationHandlers count]];
self.pr_valueObservationHandlers = [self.pr_valueObservationHandlers arrayByAddingObject:valueObservationHandler];
}
- (void) removeObservationHandlerForSelector:(SEL)selector
{
[self pr_safelyRemoveObserver:self forKeyPath:NSStringFromSelector(selector)];
// TODO: find some way of removing that block from _pr_valueObservationHandlers without breaking the context/index casting
// because developers will expect that block-retained memory to be freed when this method is called
}
@end
//
// NSObject+PRKeyValueObserving_Internal.m
// PRKeyValueObserving
//
// Created by Paul Rehkugler on 3/8/14.
// Copyright (c) 2014 Paul Rehkugler. All rights reserved.
//
#import "NSObject+PRKeyValueObserving.h"
#import <objc/runtime.h>
@implementation NSObject (PRKeyValueObserving_Internal)
#pragma mark - Method Swizzling
+ (void)load
{
// swizzle observeValueForKeyPath:ofObject:change:context:
// with _pr_observeValueForKeyPath:ofObject:change:context:
// just in case any NSObject subclasses override it
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSelector = @selector(observeValueForKeyPath:ofObject:change:context:);
SEL swizzledSelector = @selector(pr_observeValueForKeyPath:ofObject:change:context:);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
BOOL didAddMethod = class_addMethod(class, originalSelector,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAddMethod)
{
class_replaceMethod(class, swizzledSelector,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
}
else
{
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}
- (void) pr_observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
// at runtime this will correspond to observeValueForKeyPath:ofObject:change:context:
// because of method swizzling
// TODO: I _really_ don't like casting from (void *) to (NSUInteger) here
NSUInteger index = (NSUInteger) context;
NSArray *valueObservationHandlers = [self pr_valueObservationHandlers];
// if this context matches something we are watching
if (index < [valueObservationHandlers count])
{
// grab the corresponding valueObservationHandler
PRValueObservationHandler valueObservationHandler = valueObservationHandlers[index];
if (valueObservationHandler)
{
valueObservationHandler(change);
}
}
// if this KVO notification belongs to some other class
else
{
// call the original observeValueForKeyPath:ofObject:change:context: implementation
[self pr_observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
// allow objects that aren't observing keyPath to be removed safely
// because there currently isn't a way to check if an object is observing a keyPath
- (void) pr_safelyRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath
{
@try
{
[self removeObserver:observer forKeyPath:keyPath];
}
@catch (NSException * __unused exception) { }
}
#pragma mark - "Property" Accessors
// These use associated objects to simulate properties on categories
// for more info: http://nshipster.com/associated-objects/
- (NSArray *) pr_valueObservationHandlers
{
NSArray *valueObservationHandlers = objc_getAssociatedObject(self, @selector(pr_valueObservationHandlers));
// lazy initialization
if (!valueObservationHandlers)
{
valueObservationHandlers = @[];
[self setPr_valueObservationHandlers:valueObservationHandlers];
}
return valueObservationHandlers;
}
- (void) setPr_valueObservationHandlers:(NSArray *)pr_valueObservationHandlers
{
objc_setAssociatedObject(self, @selector(pr_valueObservationHandlers), pr_valueObservationHandlers, OBJC_ASSOCIATION_RETAIN);
}
@end
@irace
Copy link

irace commented Mar 9, 2014

Looks pretty good. A couple of points:

  • Using an NSUInteger as your context seems potentially problematic. Since it's still possible for code to call observeValueForKeyPath:ofObject:change:context: directly instead of using your block-based methods, what's to prevent a collision? Static pointers are usually used for context values which alleviates this concern in normal usage. Almost certainly not the case but still technically possible that NSObject could use 0 as a KVO context internally, which would break here.
  • Still requires the developer to remember to call removeObservationHandlerForSelector:. I suppose you could swizzle dealloc, iterate over the handler array and try to remove them automatically.

Looks like a fun exercise/learning experience but I'd probably use Facebook's KVOController instead.

@paulrehkugler
Copy link
Author

@irace I definitely agree with your NSUInteger context statement. I'm thinking that I should instead make a string hash and use that with a NSMutableDictionary of PRValueObservationHandlers, instead of storing those blocks in an array. It also allows for easy deletion in removeObservationHandlerForSelector: without messing up the other index/contexts.

It is possible to swizzle dealloc - that's a great idea.

I've never heard of Facebook's KVOController; that actually looks pretty good. However, I prefer to keep something close to the standard KVO API (i.e. uses NSObject instead of a custom KVOController object).

@irace
Copy link

irace commented Mar 9, 2014

What are you going to hash, specifically? The memory location of the handler block once it's copied over to the heap? If you're just using the selector, you're still susceptible to collisions in your class hierarchy (e.g. a scroll view subclass observes contentSize and so does a subclass of your subclass). Could probably just use [NSProcessInfo globallyUniqueString].

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment