// // NSObject+ProcObservation.m // Version 1.0 // // Andy Matuschak // andy@andymatuschak.org // Public domain because I love you. Let me know how you use it. // #import "NSObject+ProcObservation.h" #import #import #import @interface AMRubyObserverTrampoline : NSObject { __weak id observee; NSString *keyPath; id proc; NSOperationQueue *queue; NSKeyValueObservingOptions options; } - (AMRubyObserverTrampoline*)initObservingObject:(id)obj keyPath:(NSString*)newKeyPath options:(NSKeyValueObservingOptions)newOptions onQueue:(NSOperationQueue*)newQueue proc:(id)newProc; - (void)cancelObservation; @end @implementation AMRubyObserverTrampoline static NSString *AMRubyObserverTrampolineContext = @"AMRubyObserverTrampolineContext"; - (AMRubyObserverTrampoline*)initObservingObject:(id)obj keyPath:(NSString*)newKeyPath options:(NSKeyValueObservingOptions)newOptions onQueue:(NSOperationQueue*)newQueue proc:(id)newProc { self = [super init]; if (self != nil) { proc = newProc; keyPath = [newKeyPath copy]; queue = [newQueue retain]; observee = obj; options = newOptions; // Clear out our customized options before passing them on. // From MYUtilities. newOptions &= ~MYKeyValueObservingOptionOnce; [observee addObserver:self forKeyPath:keyPath options:newOptions context:AMRubyObserverTrampolineContext]; } return self; } - (void)observeValueForKeyPath:(NSString*)aKeyPath ofObject:(id)object change:(NSDictionary*)change context:(void*)context { if (context != AMRubyObserverTrampolineContext) { [super observeValueForKeyPath:aKeyPath ofObject:object change:change context:context]; return; } // Not sure if we really need this on the next run loop iteration. Seemed right. if (options & MYKeyValueObservingOptionOnce) [self performSelector:@selector(cancelObservation) withObject:nil afterDelay:0.0]; if (queue) [queue addOperationWithBlock:^{ [proc performRubySelector:@selector(call:) withArguments:object, change]; }]; else [proc performRubySelector:@selector(call:) withArguments:object, change]; } - (void)cancelObservation { [observee removeObserver:self forKeyPath:keyPath]; } - (void)dealloc { [self cancelObservation]; [proc release]; [keyPath release]; [queue release]; [super dealloc]; } @end static NSString *AMRubyObserverMapKey = @"org.andymatuschak.observerMapRuby"; @implementation NSObject (AMRubyBlockObservation) - (NSString *)addObserverForKeyPath:(NSString *)keyPath proc:(id)proc { return [self addObserverForKeyPath:keyPath options:0 onQueue:nil proc:proc]; } - (NSString*)addObserverForKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options proc:(id)proc { return [self addObserverForKeyPath:keyPath options:options onQueue:nil proc:proc]; } - (NSString*)addObserverForKeyPath:(NSString*)keyPath onQueue:(NSOperationQueue*)queue proc:(id)proc { return [self addObserverForKeyPath:keyPath options:0 onQueue:queue proc:proc]; } - (NSString*)addObserverForKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options onQueue:(NSOperationQueue*)queue proc:(id)proc { NSString *token = [[NSProcessInfo processInfo] globallyUniqueString]; dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ if (!objc_getAssociatedObject(self, AMRubyObserverMapKey)) objc_setAssociatedObject(self, AMRubyObserverMapKey, [NSMutableDictionary dictionary], OBJC_ASSOCIATION_RETAIN); AMRubyObserverTrampoline *trampoline = [[[AMRubyObserverTrampoline alloc] initObservingObject:self keyPath:keyPath options:options onQueue:queue proc:proc] autorelease]; [objc_getAssociatedObject(self, AMRubyObserverMapKey) setObject:trampoline forKey:token]; }); return token; } - (void)removeObserverWithProcToken:(NSString *)token { NSMutableDictionary *observationDictionary = objc_getAssociatedObject(self, AMRubyObserverMapKey); AMRubyObserverTrampoline *trampoline = [observationDictionary objectForKey:token]; if (!trampoline) { NSLog(@"Tried to remove non-existent observer on %@ for token %@", self, token); return; } [trampoline cancelObservation]; [observationDictionary removeObjectForKey:token]; } @end