Forked from andymatuschak/NSObject+BlockObservation.h
Created
September 14, 2011 20:19
-
-
Save bewebste/1217658 to your computer and use it in GitHub Desktop.
KVO+Blocks
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+BlockObservation.h | |
// Version 1.0 | |
// | |
// Andy Matuschak | |
// [email protected] | |
// 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// NSObject+BlockObservation.h | |
// Version 1.0 | |
// | |
// Andy Matuschak | |
// [email protected] | |
// 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