// // GTMNSObject+KeyValueObserving.h // // Copyright 2009 Google Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); you may not // use this file except in compliance with the License. You may obtain a copy // of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the // License for the specific language governing permissions and limitations under // the License. // // // MAKVONotificationCenter.m // MAKVONotificationCenter // // Created by Michael Ash on 10/15/08. // // This code is based on code by Michael Ash. // See comment in header. #import "GTMNSObject+KeyValueObserving.h" #import "GTMDefines.h" #import "GTMDebugSelectorValidation.h" #import "GTMObjC2Runtime.h" // A singleton that works as a dispatch center for KVO // -[NSObject observeValueForKeyPath:ofObject:change:context:] and turns them // into selector dispatches. It stores a collection of // GTMKeyValueObservingHelpers, and keys them via the key generated by // -dictionaryKeyForObserver:ofObject:forKeyPath:selector. @interface GTMKeyValueObservingCenter : NSObject { @private NSMutableDictionary *observerHelpers_; } + (id)defaultCenter; - (void)addObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options; - (void)removeObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector; - (id)dictionaryKeyForObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector; @end @interface GTMKeyValueObservingHelper : NSObject { @private __weak id observer_; SEL selector_; id userInfo_; id target_; NSString* keyPath_; } - (id)initWithObserver:(id)observer object:(id)target keyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options; - (void)deregister; @end @interface GTMKeyValueChangeNotification () - (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object userInfo:(id)userInfo change:(NSDictionary *)change; @end @implementation GTMKeyValueObservingHelper // For info how and why we use these statics: // http://lists.apple.com/archives/cocoa-dev/2006/Jul/msg01038.html static char GTMKeyValueObservingHelperContextData; static char* GTMKeyValueObservingHelperContext = >MKeyValueObservingHelperContextData; - (id)initWithObserver:(id)observer object:(id)target keyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { if((self = [super init])) { observer_ = observer; selector_ = selector; userInfo_ = [userInfo retain]; target_ = target; keyPath_ = [keyPath retain]; [target addObserver:self forKeyPath:keyPath options:options context:GTMKeyValueObservingHelperContext]; } return self; } - (NSString *)description { return [NSString stringWithFormat: @"%@ ", [self class], observer_, keyPath_, target_, NSStringFromSelector(selector_)]; } - (void)dealloc { [userInfo_ release]; [keyPath_ release]; [super dealloc]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if(context == GTMKeyValueObservingHelperContext) { GTMKeyValueChangeNotification *notification = [[GTMKeyValueChangeNotification alloc] initWithKeyPath:keyPath ofObject:object userInfo:userInfo_ change:change]; [observer_ performSelector:selector_ withObject:notification]; [notification release]; } else { // COV_NF_START // There's no way this should ever be called. // If it is, the call will go up to NSObject which will assert. [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; // COV_NF_END } } - (void)deregister { [target_ removeObserver:self forKeyPath:keyPath_]; } @end @implementation GTMKeyValueObservingCenter + (id)defaultCenter { static GTMKeyValueObservingCenter *center = nil; if(!center) { // do a bit of clever atomic setting to make this thread safe // if two threads try to set simultaneously, one will fail // and the other will set things up so that the failing thread // gets the shared center GTMKeyValueObservingCenter *newCenter = [[self alloc] init]; if(!objc_atomicCompareAndSwapGlobalBarrier(nil, newCenter, (void *)¢er)) { [newCenter release]; // COV_NF_LINE no guarantee we'll hit this line } } return center; } - (id)init { if((self = [super init])) { observerHelpers_ = [[NSMutableDictionary alloc] init]; } return self; } // COV_NF_START // Singletons don't get deallocated - (void)dealloc { [observerHelpers_ release]; [super dealloc]; } // COV_NF_END - (id)dictionaryKeyForObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector { NSString *key = [NSString stringWithFormat:@"%p:%p:%@:%p", observer, target, keyPath, selector]; return key; } - (void)addObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { GTMKeyValueObservingHelper *helper = [[GTMKeyValueObservingHelper alloc] initWithObserver:observer object:target keyPath:keyPath selector:selector userInfo:userInfo options:options]; id key = [self dictionaryKeyForObserver:observer ofObject:target forKeyPath:keyPath selector:selector]; @synchronized(self) { #if DEBUG GTMKeyValueObservingHelper *oldHelper = [observerHelpers_ objectForKey:key]; if (oldHelper) { _GTMDevLog(@"%@ already observing %@ forKeyPath %@", observer, target, keyPath); } #endif // DEBUG [observerHelpers_ setObject:helper forKey:key]; } [helper release]; } - (void)removeObserver:(id)observer ofObject:(id)target forKeyPath:(NSString *)keyPath selector:(SEL)selector { id key = [self dictionaryKeyForObserver:observer ofObject:target forKeyPath:keyPath selector:selector]; GTMKeyValueObservingHelper *helper = nil; @synchronized(self) { helper = [[observerHelpers_ objectForKey:key] retain]; #if DEBUG if (!helper) { _GTMDevLog(@"%@ was not observing %@ with keypath %@", observer, target, keyPath); } #endif // DEBUG [observerHelpers_ removeObjectForKey:key]; } [helper deregister]; [helper release]; } @end @implementation NSObject (GTMKeyValueObservingAdditions) - (void)gtm_addObserver:(id)observer forKeyPath:(NSString *)keyPath selector:(SEL)selector userInfo:(id)userInfo options:(NSKeyValueObservingOptions)options { _GTMDevAssert(observer && [keyPath length] && selector, @"Missing observer, keyPath, or selector"); GTMKeyValueObservingCenter *center = [GTMKeyValueObservingCenter defaultCenter]; GTMAssertSelectorNilOrImplementedWithArguments(observer, selector, @encode(GTMKeyValueChangeNotification *), NULL); [center addObserver:observer ofObject:self forKeyPath:keyPath selector:selector userInfo:userInfo options:options]; } - (void)gtm_removeObserver:(id)observer forKeyPath:(NSString *)keyPath selector:(SEL)selector { _GTMDevAssert(observer && [keyPath length] && selector, @"Missing observer, keyPath, or selector"); GTMKeyValueObservingCenter *center = [GTMKeyValueObservingCenter defaultCenter]; GTMAssertSelectorNilOrImplementedWithArguments(observer, selector, @encode(GTMKeyValueChangeNotification *), NULL); [center removeObserver:observer ofObject:self forKeyPath:keyPath selector:selector]; } @end @implementation GTMKeyValueChangeNotification - (id)initWithKeyPath:(NSString *)keyPath ofObject:(id)object userInfo:(id)userInfo change:(NSDictionary *)change { if ((self = [super init])) { keyPath_ = [keyPath copy]; object_ = [object retain]; userInfo_ = [userInfo retain]; change_ = [change retain]; } return self; } - (void)dealloc { [keyPath_ release]; [object_ release]; [userInfo_ release]; [change_ release]; [super dealloc]; } - (id)copyWithZone:(NSZone *)zone { return [[[self class] allocWithZone:zone] initWithKeyPath:keyPath_ ofObject:object_ userInfo:userInfo_ change:change_]; } - (BOOL)isEqual:(id)object { return ([keyPath_ isEqualToString:[object keyPath]] && [object_ isEqual:[object object]] && [userInfo_ isEqual:[object userInfo]] && [change_ isEqual:[object change]]); } - (NSString *)description { return [NSString stringWithFormat: @"%@ ", [self class], object_, keyPath_, userInfo_, change_]; } - (NSUInteger)hash { return [keyPath_ hash] + [object_ hash] + [userInfo_ hash] + [change_ hash]; } - (NSString *)keyPath { return keyPath_; } - (id)object { return object_; } - (id)userInfo { return userInfo_; } - (NSDictionary *)change { return change_; } @end #ifdef DEBUG // This category exists to attempt to help debug tricky KVO issues. // Set the GTMDebugKVO environment variable to 1 and you will get a whole // pile of KVO notifications that may help you track down problems. @interface NSObject (GTMDebugKeyValueObserving) @end @implementation NSObject (GTMDebugKeyValueObserving) + (void)load { NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSDictionary *env = [[NSProcessInfo processInfo] environment]; id debugKeyValue = [env valueForKey:@"GTMDebugKVO"]; BOOL debug = NO; if ([debugKeyValue isKindOfClass:[NSNumber class]]) { debug = [debugKeyValue intValue] != 0 ? YES : NO; } else if ([debugKeyValue isKindOfClass:[NSString class]]) { debug = ([debugKeyValue hasPrefix:@"Y"] || [debugKeyValue hasPrefix:@"T"] || [debugKeyValue intValue]); } if (debug) { Class cls = [NSObject class]; SEL addSelector = NSSelectorFromString(@"addObserver:forKeyPath:options:context:"); SEL removeSelector = NSSelectorFromString(@"removeObserver:forKeyPath:"); SEL debugAddSelector = NSSelectorFromString(@"_gtmDebugAddObserver:forKeyPath:options:context:"); SEL debugRemoveSelector = NSSelectorFromString(@"_gtmDebugRemoveObserver:forKeyPath:"); SEL willChangeValueSelector = NSSelectorFromString(@"willChangeValueForKey:"); SEL didChangeValueSelector = NSSelectorFromString(@"didChangeValueForKey:"); SEL debugWillChangeValueSelector = NSSelectorFromString(@"_gtmDebugWillChangeValueForKey:"); SEL debugDidChangeValueSelector = NSSelectorFromString(@"_gtmDebugDidChangeValueForKey:"); Method m1 = class_getInstanceMethod(cls, addSelector); Method m2 = class_getInstanceMethod(cls, debugAddSelector); method_exchangeImplementations(m1, m2); m1 = class_getInstanceMethod(cls, removeSelector); m2 = class_getInstanceMethod(cls, debugRemoveSelector); method_exchangeImplementations(m1, m2); m1 = class_getInstanceMethod(cls, willChangeValueSelector); m2 = class_getInstanceMethod(cls, debugWillChangeValueSelector); method_exchangeImplementations(m1, m2); m1 = class_getInstanceMethod(cls, didChangeValueSelector); m2 = class_getInstanceMethod(cls, debugDidChangeValueSelector); method_exchangeImplementations(m1, m2); debugAddSelector = NSSelectorFromString(@"_gtmDebugArrayAddObserver:forKeyPath:options:context:"); debugRemoveSelector = NSSelectorFromString(@"_gtmDebugArrayRemoveObserver:forKeyPath:"); cls = [NSArray class]; m1 = class_getInstanceMethod(cls, addSelector); m2 = class_getInstanceMethod(cls, debugAddSelector); method_exchangeImplementations(m1, m2); m1 = class_getInstanceMethod(cls, removeSelector); m2 = class_getInstanceMethod(cls, debugRemoveSelector); method_exchangeImplementations(m1, m2); } [pool release]; } - (void)_gtmDebugAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { _GTMDevLog(@"Adding observer %@ to %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugAddObserver:observer forKeyPath:keyPath options:options context:context]; } - (void)_gtmDebugArrayAddObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context { _GTMDevLog(@"Array adding observer %@ to %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugArrayAddObserver:observer forKeyPath:keyPath options:options context:context]; } - (void)_gtmDebugRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { _GTMDevLog(@"Removing observer %@ from %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugRemoveObserver:observer forKeyPath:keyPath]; } - (void)_gtmDebugArrayRemoveObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath { _GTMDevLog(@"Array removing observer %@ from %@ keypath '%@'", observer, self, keyPath); [self _gtmDebugArrayRemoveObserver:observer forKeyPath:keyPath]; } - (void)_gtmDebugWillChangeValueForKey:(NSString*)key { _GTMDevLog(@"Will change '%@' of %@", key, self); [self _gtmDebugWillChangeValueForKey:key]; } - (void)_gtmDebugDidChangeValueForKey:(NSString*)key { _GTMDevLog(@"Did change '%@' of %@", key, self); [self _gtmDebugDidChangeValueForKey:key]; } #endif // DEBUG @end