Skip to content

Instantly share code, notes, and snippets.

@andymatuschak
Created July 23, 2009 22:11
Show Gist options
  • Save andymatuschak/153676 to your computer and use it in GitHub Desktop.
Save andymatuschak/153676 to your computer and use it in GitHub Desktop.
KVO+Blocks
//
// NSObject+BlockObservation.h
// Version 1.0
//
// Andy Matuschak
// andy@andymatuschak.org
// 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
// andy@andymatuschak.org
// 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
@jkp
Copy link

jkp commented Apr 27, 2010

Great work Andy. I googled for an official Apple API and found the next best thing. So much nicer that the horrible API in the box.

@holleB
Copy link

holleB commented Mar 29, 2011

Very nice library. I just don't understand -void cancelObservation. dispatch_once_t as an instance variable is explicitly undefined behavior in the standard documentation. If you want to make it thread-safe, why not just dispatch on the AMObserverMutationQueue?

@andymatuschak
Copy link
Author

The dispatch_once usage should work fine: the cancellationPredicate is initialized to 0 when the object is created and exists on a per-object basis. Syncing onto the mutation queue isn't good enough because I'm trying to ensure that the methods body only executes once per instance.

@jonsterling
Copy link

Andy, thanks for this! It's come in useful a few times. I've written a few extensions that add easier cleanup (relating the tokens to the observer, and then having the observer send -cleanupObservation to itself in -dealloc) as well as naïve bindings. After I've tested them more thoroughly and cleaned things up, I'll probably fork this gist or something.

@peternlewis
Copy link

Nice work, thanks for this. A couple comments:

AMObserverMapKey should not be an NSString, or any NSObject, it makes for hassles with ARC which doesn't like converting to/from a void*. Just make it a static int, and then use the address of that as the key.

Rather than use [[NSProcessInfo processInfo] globallyUniqueString], why not just use a static long counter that you increment (inside the critical region) and use NSNumber as your key.

Regardless, thanks for a very helpful chunk of code!

@evanlong
Copy link

I believe @holleB's assessment of the undefined behavior surrounding dispatch_once_t usage as an ivar is correct.

Per Greg Parker: "[t]he implementation of dispatch_once() requires that the dispatch_once_t is zero, and has never been non-zero."

http://stackoverflow.com/a/19845164/63804

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