public
Last active

KVO+Blocks

  • Download Gist
NSObject+BlockObservation.h
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
//
// 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.m
Objective-C
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
//
// 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

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.

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?

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.

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.

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!

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.