-
-
Save paulrehkugler/9442834 to your computer and use it in GitHub Desktop.
// | |
// 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 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).
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]
.
Looks pretty good. A couple of points:
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 thatNSObject
could use0
as a KVO context internally, which would break here.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.