Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save tternes/7254187 to your computer and use it in GitHub Desktop.
Save tternes/7254187 to your computer and use it in GitHub Desktop.
//
// NSObject+BlockObservation.h
// Version 1.0
//
// Andy Matuschak
// [email protected]
// Public domain because I love you. Let me know how you use it.
//
#import <Cocoa/Cocoa.h>
typedef NSString AMBlockToken;
typedef void (^AMBlockTask)(id obj, NSDictionary *change);
@interface NSObject (AMBlockObservation)
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath task:(AMBlockTask)task;
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task;
- (void)removeObserverWithBlockToken:(AMBlockToken *)token;
@end
//
// NSObject+BlockObservation.h
// Version 1.0
//
// Andy Matuschak
// [email protected]
// Public domain because I love you. Let me know how you use it.
//
#import "NSObject+BlockObservation.h"
#import <dispatch/dispatch.h>
#import <objc/runtime.h>
@interface AMObserverTrampoline : NSObject
{
__weak id observee;
NSString *keyPath;
AMBlockTask task;
NSOperationQueue *queue;
dispatch_once_t cancellationPredicate;
}
- (AMObserverTrampoline *)initObservingObject:(id)obj keyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task;
- (void)cancelObservation;
@end
@implementation AMObserverTrampoline
static NSString *AMObserverTrampolineContext = @"AMObserverTrampolineContext";
- (AMObserverTrampoline *)initObservingObject:(id)obj keyPath:(NSString *)newKeyPath onQueue:(NSOperationQueue *)newQueue task:(AMBlockTask)newTask
{
if (!(self = [super init])) return nil;
task = [newTask copy];
keyPath = [newKeyPath copy];
queue = [newQueue retain];
observee = obj;
cancellationPredicate = 0;
[observee addObserver:self forKeyPath:keyPath options:0 context:AMObserverTrampolineContext];
return self;
}
- (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == AMObserverTrampolineContext)
{
if (queue)
[queue addOperationWithBlock:^{ task(object, change); }];
else
task(object, change);
}
}
- (void)cancelObservation
{
dispatch_once(&cancellationPredicate, ^{
[observee removeObserver:self forKeyPath:keyPath];
observee = nil;
});
}
- (void)dealloc
{
[self cancelObservation];
[task release];
[keyPath release];
[queue release];
[super dealloc];
}
@end
static NSString *AMObserverMapKey = @"org.andymatuschak.observerMap";
static dispatch_queue_t AMObserverMutationQueue = NULL;
static dispatch_queue_t AMObserverMutationQueueCreatingIfNecessary()
{
static dispatch_once_t queueCreationPredicate = 0;
dispatch_once(&queueCreationPredicate, ^{
AMObserverMutationQueue = dispatch_queue_create("org.andymatuschak.observerMutationQueue", 0);
});
return AMObserverMutationQueue;
}
@implementation NSObject (AMBlockObservation)
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath task:(AMBlockTask)task
{
return [self addObserverForKeyPath:keyPath onQueue:nil task:task];
}
- (AMBlockToken *)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task
{
AMBlockToken *token = [[NSProcessInfo processInfo] globallyUniqueString];
dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{
NSMutableDictionary *dict = objc_getAssociatedObject(self, AMObserverMapKey);
if (!dict)
{
dict = [[NSMutableDictionary alloc] init];
objc_setAssociatedObject(self, AMObserverMapKey, dict, OBJC_ASSOCIATION_RETAIN);
[dict release];
}
AMObserverTrampoline *trampoline = [[AMObserverTrampoline alloc] initObservingObject:self keyPath:keyPath onQueue:queue task:task];
[dict setObject:trampoline forKey:token];
[trampoline release];
});
return token;
}
- (void)removeObserverWithBlockToken:(AMBlockToken *)token
{
dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{
NSMutableDictionary *observationDictionary = objc_getAssociatedObject(self, AMObserverMapKey);
AMObserverTrampoline *trampoline = [observationDictionary objectForKey:token];
if (!trampoline)
{
NSLog(@"[NSObject(AMBlockObservation) removeObserverWithBlockToken]: Ignoring attempt to remove non-existent observer on %@ for token %@.", self, token);
return;
}
[trampoline cancelObservation];
[observationDictionary removeObjectForKey:token];
// Due to a bug in the obj-c runtime, this dictionary does not get cleaned up on release when running without GC.
if ([observationDictionary count] == 0)
objc_setAssociatedObject(self, AMObserverMapKey, nil, OBJC_ASSOCIATION_RETAIN);
});
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment