Skip to content

Embed URL

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
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

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

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

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

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

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Something went wrong with that request. Please try again.