Skip to content

Instantly share code, notes, and snippets.

@bewebste
Created September 15, 2011 22:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bewebste/1220613 to your computer and use it in GitHub Desktop.
Save bewebste/1220613 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.
//
/*
Summary of changes made by Brian Webster from the original category:
- Rather than returning a token block that needs to be retained in order to remove the observer
later on, you instead pass in your own string identifier that can be used later when removing.
I find this easier than having to keep track of tokens.
- Added another parameter for specifying NSKeyValueObservingOptions
- Added a method for observing key paths on multiple objects in a to-many KVC relationship
- Removed mutation queue, as it could cause deadlocks when used in conjunction with
NSKeyValueObservingOptionInitial
*/
#import <Cocoa/Cocoa.h>
typedef void (^AMBlockTask)(id obj, NSDictionary *change);
@interface NSObject (AMBlockObservation)
- (void)addObserverForKeyPath:(NSString *)keyPath task:(void (^)(id obj, NSDictionary *change))task;
- (void)addObserverForKeyPath:(NSString *)keyPath identifier:(NSString*)inIdentifier task:(AMBlockTask)task;
- (void)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task;
- (void)addObserverForKeyPath:(NSString*)inKeyPath withOptions:(NSKeyValueObservingOptions)inOptions identifier:(NSString*)inIdentifier onQueue:(NSOperationQueue*)inQueue task:(AMBlockTask)task;
- (void)removeObserverForKeyPath:(NSString*)inKeyPath identifier:(NSString*)inIdentifier;
- (void)addObserverForKey:(NSString*)key subKeyPaths:(NSArray*)subKeyPaths withOptions:(NSKeyValueObservingOptions)options onQueue:(NSOperationQueue*)queue task:(void (^)(id object, NSString* key, NSDictionary* change))task;
- (void)removeObserverForKey:(NSString*)key subKeyPaths:(NSArray*)subKeyPaths;
@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.
//
#if __has_feature(objc_arc)
#error This file must be compiled with ARC disabled
#endif
#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 options:(NSKeyValueObservingOptions)inOptions onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task;
- (void)cancelObservation;
@end
//Removed this from the original implementation because it could cause deadlock when using
//the NSKeyValueObservationOptionsInitial option. Add an observer, that triggers a KVO
//notification, that leads to another add/remove within the first dispatch_sync() call, and
//voila, deadlock
//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 AMObserverTrampoline
static NSString *AMObserverTrampolineContext = @"AMObserverTrampolineContext";
- (AMObserverTrampoline *)initObservingObject:(id)obj keyPath:(NSString *)newKeyPath options:(NSKeyValueObservingOptions)inOptions 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:inOptions context:AMObserverTrampolineContext];
return self;
}
- (NSString *)description
{
return [NSString stringWithFormat:@"<AMObserverTrampoline %p: observee %p, keyPath %@>", self, observee, keyPath];
}
- (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == AMObserverTrampolineContext)
{
// NSLog(@"key path %@ changed on object %@ with change %@", aKeyPath, [object shortDescription], change);
if (queue)
[queue addOperationWithBlock:^{ task(object, change); }];
else
task(object, change);
}
else
{
[super observeValueForKeyPath:aKeyPath ofObject:object change:change context:context];
}
}
- (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";
@implementation NSObject (AMBlockObservation)
- (id)keyForTarget:(id)inTarget keyPath:(NSString *)inKeyPath identifier:(NSString *)inIdentifier;
{
NSAssert(inKeyPath != NULL, @"No key path");
NSAssert(inTarget != NULL, @"No target");
return([NSString stringWithFormat:@"%p:%@:%@", inTarget, inKeyPath, inIdentifier]);
}
- (NSMutableDictionary*)observationDictionary
{
NSMutableDictionary* observationDictionary = objc_getAssociatedObject(self, AMObserverMapKey);
if (observationDictionary == nil)
{
observationDictionary = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, AMObserverMapKey, observationDictionary, OBJC_ASSOCIATION_RETAIN);
}
return observationDictionary;
}
- (void)addObserverForKeyPath:(NSString*)inKeyPath withOptions:(NSKeyValueObservingOptions)inOptions identifier:(NSString*)inIdentifier onQueue:(NSOperationQueue*)inQueue task:(AMBlockTask)task
{
id trampolineKey = [self keyForTarget:self keyPath:inKeyPath identifier:inIdentifier];
//See note at AMObserverMutationQueueCreatingIfNecessary()
// dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{
NSMutableDictionary* observationDictionary = [self observationDictionary];
NSAssert1([observationDictionary objectForKey:trampolineKey] == nil, @"Tried to add observation twice for key %@", trampolineKey);
AMObserverTrampoline *trampoline = [[AMObserverTrampoline alloc] initObservingObject:self keyPath:inKeyPath options:inOptions onQueue:inQueue task:task];
[observationDictionary setObject:trampoline forKey:trampolineKey];
[trampoline release];
// });
}
- (void)addObserverForKeyPath:(NSString *)keyPath task:(AMBlockTask)task
{
[self addObserverForKeyPath:keyPath onQueue:nil task:task];
}
- (void)addObserverForKeyPath:(NSString *)keyPath identifier:(NSString*)inIdentifier task:(AMBlockTask)task
{
[self addObserverForKeyPath:keyPath withOptions:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld identifier:inIdentifier onQueue:nil task:task];
}
- (void)addObserverForKeyPath:(NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task
{
[self addObserverForKeyPath:keyPath withOptions:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld identifier:nil onQueue:queue task:task];
}
- (void)removeObserverForKeyPath:(NSString*)inKeyPath identifier:(NSString*)inIdentifier
{
//See note at AMObserverMutationQueueCreatingIfNecessary()
// dispatch_sync(AMObserverMutationQueueCreatingIfNecessary(), ^{
id trampolineKey = [self keyForTarget:self keyPath:inKeyPath identifier:inIdentifier];
NSMutableDictionary *observationDictionary = objc_getAssociatedObject(self, AMObserverMapKey);
AMObserverTrampoline *trampoline = [observationDictionary objectForKey:trampolineKey];
if (!trampoline)
{
[NSException raise:NSInternalInconsistencyException format:@"Tried to remove non-existent observer on %@ for token %@", self, trampolineKey];
}
[trampoline cancelObservation];
[observationDictionary removeObjectForKey:trampolineKey];
// 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);
// });
}
#pragma mark --- Multi-value observing ---
- (void)updateObservationsWithChange:(NSDictionary*)inChange subKeyPaths:(NSArray*)subKeyPaths options:(NSKeyValueObservingOptions)options identifier:(NSString*)identifier onQueue:(NSOperationQueue*)queue task:(void (^)(id object, NSString* keyPath, NSDictionary* inChange))task
{
NSMutableSet* removedObjects;
NSMutableSet* addedObjects;
id object;
NSString* keyPath;
removedObjects = [NSMutableSet setWithArray:[inChange objectForKey:NSKeyValueChangeOldKey]];
[removedObjects minusSet:[NSSet setWithArray:[inChange objectForKey:NSKeyValueChangeNewKey]]];
for (object in removedObjects)
{
for (keyPath in subKeyPaths)
[object removeObserverForKeyPath:keyPath identifier:identifier];
}
addedObjects = [NSMutableSet setWithArray:[inChange objectForKey:NSKeyValueChangeNewKey]];
[addedObjects minusSet:[NSSet setWithArray:[inChange objectForKey:NSKeyValueChangeOldKey]]];
for (object in addedObjects)
{
for (keyPath in subKeyPaths)
{
[object addObserverForKeyPath:keyPath withOptions:options identifier:identifier onQueue:queue task:^(id obj, NSDictionary *change) {
task(obj, keyPath, change);
}];
}
}
}
/*!
@method addObserverForKeyPath:subKeyPaths:withOptions:identifier:onQueue::
@abstract Observes a number of key paths of a to-many relationship
@discussion This method is used in a situation where you have a to-many relationship, and you want to observe one or more keys on each object in that to-many relationship. In addition to observing the existing objects, this will also track objects newly added to the to-many relationship, and stop observing ones that are removed.
@param key The key of the to-many relationship to track
@param subKeyPaths An array of key paths. For each object in the to-many relationship, each of the key paths will be observed.
@param options KVO observing options
@param queue The queue on which KVO callbacks should be executed
@param task The block to be executed when any of the key paths on any of the observed objects changes
*/
- (void)addObserverForKey:(NSString*)key subKeyPaths:(NSArray*)subKeyPaths withOptions:(NSKeyValueObservingOptions)options onQueue:(NSOperationQueue*)queue task:(void (^)(id object, NSString* key, NSDictionary* change))task
{
NSString* identifier = [subKeyPaths componentsJoinedByString:@"+"];
[self addObserverForKeyPath:key withOptions:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew identifier:identifier onQueue:queue task:^(id obj, NSDictionary *change) {
[self updateObservationsWithChange:change subKeyPaths:subKeyPaths options:options identifier:identifier onQueue:queue task:task];
}];
[self updateObservationsWithChange:[NSDictionary dictionaryWithObjectsAndKeys:[NSArray array], NSKeyValueChangeOldKey, [self valueForKey:key], NSKeyValueChangeNewKey, nil] subKeyPaths:subKeyPaths options:options identifier:identifier onQueue:queue task:task];
}
- (void)removeObserverForKey:(NSString*)key subKeyPaths:(NSArray*)subKeyPaths
{
[self updateObservationsWithChange:[NSDictionary dictionaryWithObjectsAndKeys:[NSArray array], NSKeyValueChangeNewKey, [self valueForKey:key], NSKeyValueChangeOldKey, nil] subKeyPaths:subKeyPaths options:0 identifier:[subKeyPaths componentsJoinedByString:@":"] onQueue:nil task:NULL];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment