diff options
author | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-02-03 17:09:43 +0000 |
---|---|---|
committer | thomasvl@gmail.com <thomasvl@gmail.com@7dc7ac4e-7543-0410-b95c-c1676fc8e2a3> | 2009-02-03 17:09:43 +0000 |
commit | 74ad2857a75567b273951be9cbe998133fbca26a (patch) | |
tree | 9aedbec980fc19be9f3eecf7acd0dfaa9f7c8067 /Foundation | |
parent | 2ae297214778005d95354f207753180edca51ec4 (diff) |
- Added GTMNSObject+KeyValueObserving to make it easier on folks to do KVO
"correctly". Based on some excellent code by Michael Ash.
http://www.mikeash.com/?page=pyblog/key-value-observing-done-right.html
This has been added for iPhone and OS X.
- Fixed up GTMSenTestCase on iPhone so that it has a description that matches
that of OCUnit.
Diffstat (limited to 'Foundation')
-rw-r--r-- | Foundation/GTMNSObject+KeyValueObserving.h | 70 | ||||
-rw-r--r-- | Foundation/GTMNSObject+KeyValueObserving.m | 350 | ||||
-rw-r--r-- | Foundation/GTMNSObject+KeyValueObservingTest.m | 106 | ||||
-rw-r--r-- | Foundation/GTMObjC2Runtime.h | 3 |
4 files changed, 528 insertions, 1 deletions
diff --git a/Foundation/GTMNSObject+KeyValueObserving.h b/Foundation/GTMNSObject+KeyValueObserving.h new file mode 100644 index 0000000..de11aeb --- /dev/null +++ b/Foundation/GTMNSObject+KeyValueObserving.h @@ -0,0 +1,70 @@ +// +// 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.h +// MAKVONotificationCenter +// +// Created by Michael Ash on 10/15/08. +// + +// This code is based on code by Michael Ash. +// Please see his excellent writeup at +// http://www.mikeash.com/?page=pyblog/key-value-observing-done-right.html +// You may also be interested in this writeup: +// http://www.dribin.org/dave/blog/archives/2008/09/24/proper_kvo_usage/ +// and the discussion on cocoa-dev that is linked to at the end of it. + +#import <Foundation/Foundation.h> + +// If you read the articles above you will see that doing KVO correctly +// is actually pretty tricky, and that Apple's documentation may not be +// completely clear as to how things should be used. Use the methods below +// to make things a little easier instead of the stock addObserver, +// removeObserver methods. +// Selector should have the following signature: +// - (void)observeNotification:(GTMKeyValueChangeNotification *)notification +@interface NSObject (GTMKeyValueObservingAdditions) + +// Use this instead of [NSObject addObserver:forKeyPath:options:context:] +- (void)gtm_addObserver:(id)observer + forKeyPath:(NSString *)keyPath + selector:(SEL)selector + userInfo:(id)userInfo + options:(NSKeyValueObservingOptions)options; +// Use this instead of [NSObject removeObserver:forKeyPath:] +- (void)gtm_removeObserver:(id)observer + forKeyPath:(NSString *)keyPath + selector:(SEL)selector; +@end + +// This is the class that is sent to your notification selector as an +// argument. +@interface GTMKeyValueChangeNotification : NSObject <NSCopying> { + @private + NSString *keyPath_; + id object_; + id userInfo_; + NSDictionary *change_; +} + +- (NSString *)keyPath; +- (id)object; +- (id)userInfo; +- (NSDictionary *)change; +@end diff --git a/Foundation/GTMNSObject+KeyValueObserving.m b/Foundation/GTMNSObject+KeyValueObserving.m new file mode 100644 index 0000000..dc6883d --- /dev/null +++ b/Foundation/GTMNSObject+KeyValueObserving.m @@ -0,0 +1,350 @@ +// +// 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 <libkern/OSAtomic.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; +} + +- (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]]); +} + +- (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 diff --git a/Foundation/GTMNSObject+KeyValueObservingTest.m b/Foundation/GTMNSObject+KeyValueObservingTest.m new file mode 100644 index 0000000..b97f7ff --- /dev/null +++ b/Foundation/GTMNSObject+KeyValueObservingTest.m @@ -0,0 +1,106 @@ +// +// GTMNSObject+KeyValueObservingTest.m +// +// 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. +// + +// +// Tester.m +// MAKVONotificationCenter +// +// Created by Michael Ash on 10/15/08. +// + +// This code is based on code by Michael Ash. +// See comment in header. + +#import "GTMSenTestCase.h" +#import "GTMNSObject+KeyValueObserving.h" +#import "GTMDefines.h" +#import "GTMUnitTestDevLog.h" + +@interface GTMNSObject_KeyValueObservingTest : GTMTestCase { + int32_t count_; + NSMutableDictionary *dict_; + __weak NSString *expectedValue_; +} +@end + +@implementation GTMNSObject_KeyValueObservingTest +- (void)setUp { + dict_ = [[NSMutableDictionary alloc] initWithObjectsAndKeys: + @"foo", @"key", + nil]; +} + +- (void)tearDown { + [dict_ release]; +} + +- (void)testSingleChange { + [dict_ gtm_addObserver:self + forKeyPath:@"key" + selector:@selector(observeValueChange:) + userInfo:@"userInfo" + options:NSKeyValueObservingOptionNew]; + expectedValue_ = @"bar"; + [dict_ setObject:expectedValue_ forKey:@"key"]; + STAssertEquals(count_, (int32_t)1, nil); + [dict_ gtm_removeObserver:self + forKeyPath:@"key" + selector:@selector(observeValueChange:)]; + [dict_ setObject:@"foo" forKey:@"key"]; + STAssertEquals(count_, (int32_t)1, nil); +} + +- (void)testRemoving { + [GTMUnitTestDevLogDebug expectPattern:@"-\\[GTMNSObject_KeyValueObservingTest" + @" testRemoving\\] was not observing.*"]; + + [dict_ gtm_removeObserver:self + forKeyPath:@"key" + selector:@selector(observeValueChange:)]; +} + +- (void)testAdding { + [dict_ gtm_addObserver:self + forKeyPath:@"key" + selector:@selector(observeValueChange:) + userInfo:@"userInfo" + options:NSKeyValueObservingOptionNew]; + [GTMUnitTestDevLogDebug expectPattern:@"-\\[GTMNSObject_KeyValueObservingTest" + @" testAdding\\] already observing.*"]; + [dict_ gtm_addObserver:self + forKeyPath:@"key" + selector:@selector(observeValueChange:) + userInfo:@"userInfo" + options:NSKeyValueObservingOptionNew]; +} + +- (void)observeValueChange:(GTMKeyValueChangeNotification *)notification { + STAssertEqualObjects([notification userInfo], @"userInfo", nil); + STAssertEqualObjects([notification keyPath], @"key", nil); + STAssertEqualObjects([notification object], dict_, nil); + NSDictionary *change = [notification change]; + NSString *value = [change objectForKey:NSKeyValueChangeNewKey]; + STAssertEqualObjects(value, expectedValue_, nil); + ++count_; + + GTMKeyValueChangeNotification *copy = [[notification copy] autorelease]; + STAssertEqualObjects(notification, copy, nil); + STAssertEquals([notification hash], [copy hash], nil); +} + +@end diff --git a/Foundation/GTMObjC2Runtime.h b/Foundation/GTMObjC2Runtime.h index 8d62abf..2c25841 100644 --- a/Foundation/GTMObjC2Runtime.h +++ b/Foundation/GTMObjC2Runtime.h @@ -47,9 +47,10 @@ #import <objc/Object.h> #endif +#import <libkern/OSAtomic.h> + #if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_5 #import "objc/Protocol.h" -#import <libkern/OSAtomic.h> OBJC_EXPORT Class object_getClass(id obj); OBJC_EXPORT const char *class_getName(Class cls); |