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 | |
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.
-rw-r--r-- | AppKit/GTMHotKeyTextField.m | 59 | ||||
-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 | ||||
-rw-r--r-- | GTM.xcodeproj/project.pbxproj | 12 | ||||
-rw-r--r-- | GTMiPhone.xcodeproj/project.pbxproj | 10 | ||||
-rw-r--r-- | ReleaseNotes.txt | 8 | ||||
-rw-r--r-- | UnitTesting/GTMSenTestCase.m | 6 |
9 files changed, 598 insertions, 26 deletions
diff --git a/AppKit/GTMHotKeyTextField.m b/AppKit/GTMHotKeyTextField.m index 0f14e48..6617b9a 100644 --- a/AppKit/GTMHotKeyTextField.m +++ b/AppKit/GTMHotKeyTextField.m @@ -20,6 +20,7 @@ #import <Carbon/Carbon.h> #import "GTMSystemVersion.h" #import "GTMObjectSingleton.h" +#import "GTMNSObject+KeyValueObserving.h" #if MAC_OS_X_VERSION_MIN_REQUIRED <= MAC_OS_X_VERSION_10_4 typedef struct __TISInputSource* TISInputSourceRef; @@ -51,7 +52,9 @@ static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL; #if GTM_SUPPORT_GC - (void)finalize { if (boundObject_ && boundKeyPath_) { - [boundObject_ removeObserver:self forKeyPath:boundKeyPath_]; + [boundObject_ gtm_removeObserver:self + forKeyPath:boundKeyPath_ + selector:@selector(hotKeyValueChanged:)]; } [super finalize]; } @@ -60,7 +63,9 @@ static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL; - (void)dealloc { if (boundObject_ && boundKeyPath_) { - [boundObject_ removeObserver:self forKeyPath:boundKeyPath_]; + [boundObject_ gtm_removeObserver:self + forKeyPath:boundKeyPath_ + selector:@selector(hotKeyValueChanged:)]; } [boundObject_ release]; [boundKeyPath_ release]; @@ -93,7 +98,9 @@ static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL; // Clean up value on unbind if ([binding isEqualToString:NSValueBinding]) { if (boundObject_ && boundKeyPath_) { - [boundObject_ removeObserver:self forKeyPath:boundKeyPath_]; + [boundObject_ gtm_removeObserver:self + forKeyPath:boundKeyPath_ + selector:@selector(hotKeyValueChanged:)]; } [boundObject_ release]; boundObject_ = nil; @@ -104,31 +111,32 @@ static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object - change:(NSDictionary *)change context:(void *)context { - - if ((object == boundObject_) && [boundKeyPath_ isEqualToString:keyPath]) { - // Our binding has changed, update - id changedValue = [change objectForKey:NSKeyValueChangeNewKey]; - // NSUserDefaultsController does not appear to pass on the new object and, - // perhaps other controllers may not, so if we get a nil or NSNull back - // here let's directly retrieve the hotKeyDict_ from the object. - if (!changedValue || changedValue == [NSNull null]) { - changedValue = [object valueForKeyPath:keyPath]; - } - [hotKeyDict_ autorelease]; - hotKeyDict_ = [changedValue copy]; - [self updateDisplayedPrettyString]; - } - -} +- (void)hotKeyValueChanged:(GTMKeyValueChangeNotification *)note { + NSDictionary *change = [note change]; + // Our binding has changed, update + id changedValue = [change objectForKey:NSKeyValueChangeNewKey]; + // NSUserDefaultsController does not appear to pass on the new object and, + // perhaps other controllers may not, so if we get a nil or NSNull back + // here let's directly retrieve the hotKeyDict_ from the object. + if (!changedValue || changedValue == [NSNull null]) { + id object = [note object]; + NSString *keyPath = [note keyPath]; + changedValue = [object valueForKeyPath:keyPath]; + } + [hotKeyDict_ autorelease]; + hotKeyDict_ = [changedValue copy]; + [self updateDisplayedPrettyString]; +} + // Private convenience method for attaching to a new binding - (void)setupBinding:(id)bound withPath:(NSString *)path { // Release previous if (boundObject_ && boundKeyPath_) { - [boundObject_ removeObserver:self forKeyPath:boundKeyPath_]; + [boundObject_ gtm_removeObserver:self + forKeyPath:boundKeyPath_ + selector:@selector(hotKeyValueChanged:)]; } [boundObject_ release]; [boundKeyPath_ release]; @@ -136,10 +144,11 @@ static CFStringRef kGTM_TISPropertyUnicodeKeyLayoutData = NULL; boundObject_ = [bound retain]; boundKeyPath_ = [path copy]; // Make ourself an observer - [boundObject_ addObserver:self + [boundObject_ gtm_addObserver:self forKeyPath:boundKeyPath_ - options:NSKeyValueObservingOptionNew - context:nil]; + selector:@selector(hotKeyValueChanged:) + userInfo:nil + options:NSKeyValueObservingOptionNew]; // Pull in any current value [hotKeyDict_ autorelease]; hotKeyDict_ = [[boundObject_ valueForKeyPath:boundKeyPath_] copy]; 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); diff --git a/GTM.xcodeproj/project.pbxproj b/GTM.xcodeproj/project.pbxproj index 31d02c5..dae5f7e 100644 --- a/GTM.xcodeproj/project.pbxproj +++ b/GTM.xcodeproj/project.pbxproj @@ -100,6 +100,9 @@ 8B5547B90DB3BB220014CC1C /* GTMAppKit+UnitTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B55479B0DB3B7A50014CC1C /* GTMAppKit+UnitTesting.m */; }; 8B58E9950E547EB000A0E02E /* GTMGetURLHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B58E9940E547EB000A0E02E /* GTMGetURLHandler.m */; }; 8B61FDC00E4CDB8000FF9C21 /* GTMStackTrace.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B61FDBF0E4CDB8000FF9C21 /* GTMStackTrace.m */; }; + 8B6C15930F356E6400E51E5D /* GTMNSObject+KeyValueObserving.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B6C15910F356E6400E51E5D /* GTMNSObject+KeyValueObserving.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8B6C15940F356E6400E51E5D /* GTMNSObject+KeyValueObserving.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C15920F356E6400E51E5D /* GTMNSObject+KeyValueObserving.m */; }; + 8B6C161C0F3580DA00E51E5D /* GTMNSObject+KeyValueObservingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C161B0F3580DA00E51E5D /* GTMNSObject+KeyValueObservingTest.m */; }; 8B6F32080DA34A1B0052CA40 /* GTMObjC2RuntimeTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6F32050DA34A1B0052CA40 /* GTMObjC2RuntimeTest.m */; }; 8B6F32160DA34C830052CA40 /* GTMMethodCheckTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6F31F10DA347720052CA40 /* GTMMethodCheckTest.m */; }; 8B6F4B630E8856CA00425D9F /* GTMDebugThreadValidation.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B6F4B610E8856CA00425D9F /* GTMDebugThreadValidation.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -395,6 +398,9 @@ 8B55479B0DB3B7A50014CC1C /* GTMAppKit+UnitTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMAppKit+UnitTesting.m"; sourceTree = "<group>"; }; 8B58E9940E547EB000A0E02E /* GTMGetURLHandler.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMGetURLHandler.m; sourceTree = "<group>"; }; 8B61FDBF0E4CDB8000FF9C21 /* GTMStackTrace.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMStackTrace.m; sourceTree = "<group>"; }; + 8B6C15910F356E6400E51E5D /* GTMNSObject+KeyValueObserving.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSObject+KeyValueObserving.h"; sourceTree = "<group>"; }; + 8B6C15920F356E6400E51E5D /* GTMNSObject+KeyValueObserving.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSObject+KeyValueObserving.m"; sourceTree = "<group>"; }; + 8B6C161B0F3580DA00E51E5D /* GTMNSObject+KeyValueObservingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSObject+KeyValueObservingTest.m"; sourceTree = "<group>"; }; 8B6F31EF0DA347720052CA40 /* GTMMethodCheck.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMMethodCheck.m; sourceTree = "<group>"; }; 8B6F31F10DA347720052CA40 /* GTMMethodCheckTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMMethodCheckTest.m; sourceTree = "<group>"; }; 8B6F31F40DA3489B0052CA40 /* GTMMethodCheck.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMMethodCheck.h; sourceTree = "<group>"; }; @@ -858,6 +864,9 @@ F413908C0D75F63C00F72B31 /* GTMNSFileManager+Path.h */, F413908D0D75F63C00F72B31 /* GTMNSFileManager+Path.m */, F413908E0D75F63C00F72B31 /* GTMNSFileManager+PathTest.m */, + 8B6C15910F356E6400E51E5D /* GTMNSObject+KeyValueObserving.h */, + 8B6C15920F356E6400E51E5D /* GTMNSObject+KeyValueObserving.m */, + 8B6C161B0F3580DA00E51E5D /* GTMNSObject+KeyValueObservingTest.m */, F42597760E23FE3A003BEA3E /* GTMNSString+FindFolder.h */, F42597770E23FE3A003BEA3E /* GTMNSString+FindFolder.m */, F42597780E23FE3A003BEA3E /* GTMNSString+FindFolderTest.m */, @@ -1044,6 +1053,7 @@ F49FA8440EEF2AB700077669 /* GTMFileSystemKQueue.h in Headers */, 8B8EC87D0EF17C270044D13F /* GTMNSFileManager+Carbon.h in Headers */, 8BA01B5E0F144BD800926923 /* GTMNSWorkspace+Running.h in Headers */, + 8B6C15930F356E6400E51E5D /* GTMNSObject+KeyValueObserving.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1386,6 +1396,7 @@ 6294454C0EDDF89A009295EA /* GTMNSArray+MergeTest.m in Sources */, F49FA88B0EEF303D00077669 /* GTMFileSystemKQueueTest.m in Sources */, 8B8EC8800EF17C2F0044D13F /* GTMNSFileManager+CarbonTest.m in Sources */, + 8B6C161C0F3580DA00E51E5D /* GTMNSObject+KeyValueObservingTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1443,6 +1454,7 @@ F49FA8450EEF2AB700077669 /* GTMFileSystemKQueue.m in Sources */, 8B8EC87E0EF17C270044D13F /* GTMNSFileManager+Carbon.m in Sources */, 8BA01B5D0F144BD800926923 /* GTMNSWorkspace+Running.m in Sources */, + 8B6C15940F356E6400E51E5D /* GTMNSObject+KeyValueObserving.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/GTMiPhone.xcodeproj/project.pbxproj b/GTMiPhone.xcodeproj/project.pbxproj index 64e0671..4221f3f 100644 --- a/GTMiPhone.xcodeproj/project.pbxproj +++ b/GTMiPhone.xcodeproj/project.pbxproj @@ -40,6 +40,8 @@ 8B5547CA0DB3BBF20014CC1C /* GTMUIKit+UnitTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B5547C70DB3BBF20014CC1C /* GTMUIKit+UnitTesting.m */; }; 8B5547CB0DB3BBF20014CC1C /* GTMUIKit+UnitTestingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B5547C90DB3BBF20014CC1C /* GTMUIKit+UnitTestingTest.m */; }; 8B5A9E200E71CB6C005DA441 /* AddressBook.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B5A9E1F0E71CB6C005DA441 /* AddressBook.framework */; }; + 8B6C18740F3769D200E51E5D /* GTMNSObject+KeyValueObserving.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C18720F3769D200E51E5D /* GTMNSObject+KeyValueObserving.m */; }; + 8B6C18750F3769D200E51E5D /* GTMNSObject+KeyValueObservingTest.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B6C18730F3769D200E51E5D /* GTMNSObject+KeyValueObservingTest.m */; }; 8B7DCEAA0DFF4C760017E983 /* GTMDevLogUnitTestingBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B7DCEA90DFF4C760017E983 /* GTMDevLogUnitTestingBridge.m */; }; 8B7DCEAD0DFF4CA60017E983 /* GTMUnitTestDevLog.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B7DCEAC0DFF4CA60017E983 /* GTMUnitTestDevLog.m */; }; 8BA5F40B0E75669000798036 /* GTMABAddressBook.m in Sources */ = {isa = PBXBuildFile; fileRef = 8BA5F4080E75669000798036 /* GTMABAddressBook.m */; }; @@ -141,6 +143,9 @@ 8B5547C80DB3BBF20014CC1C /* GTMUIKit+UnitTesting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMUIKit+UnitTesting.h"; sourceTree = "<group>"; }; 8B5547C90DB3BBF20014CC1C /* GTMUIKit+UnitTestingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMUIKit+UnitTestingTest.m"; sourceTree = "<group>"; }; 8B5A9E1F0E71CB6C005DA441 /* AddressBook.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AddressBook.framework; path = System/Library/Frameworks/AddressBook.framework; sourceTree = SDKROOT; }; + 8B6C18710F3769D200E51E5D /* GTMNSObject+KeyValueObserving.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "GTMNSObject+KeyValueObserving.h"; sourceTree = "<group>"; }; + 8B6C18720F3769D200E51E5D /* GTMNSObject+KeyValueObserving.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSObject+KeyValueObserving.m"; sourceTree = "<group>"; }; + 8B6C18730F3769D200E51E5D /* GTMNSObject+KeyValueObservingTest.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "GTMNSObject+KeyValueObservingTest.m"; sourceTree = "<group>"; }; 8B7DCEA90DFF4C760017E983 /* GTMDevLogUnitTestingBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMDevLogUnitTestingBridge.m; sourceTree = "<group>"; }; 8B7DCEAB0DFF4CA60017E983 /* GTMUnitTestDevLog.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = GTMUnitTestDevLog.h; sourceTree = "<group>"; }; 8B7DCEAC0DFF4CA60017E983 /* GTMUnitTestDevLog.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GTMUnitTestDevLog.m; sourceTree = "<group>"; }; @@ -369,6 +374,9 @@ 8BC047840DAE928A00C2D1CA /* GTMNSFileManager+Path.h */, 8BC047850DAE928A00C2D1CA /* GTMNSFileManager+Path.m */, 8BC047860DAE928A00C2D1CA /* GTMNSFileManager+PathTest.m */, + 8B6C18710F3769D200E51E5D /* GTMNSObject+KeyValueObserving.h */, + 8B6C18720F3769D200E51E5D /* GTMNSObject+KeyValueObserving.m */, + 8B6C18730F3769D200E51E5D /* GTMNSObject+KeyValueObservingTest.m */, 8BC047870DAE928A00C2D1CA /* GTMNSString+HTML.h */, 8BC047880DAE928A00C2D1CA /* GTMNSString+HTML.m */, 8BC047890DAE928A00C2D1CA /* GTMNSString+HTMLTest.m */, @@ -636,6 +644,8 @@ F417115B0ECDFF0400B9B276 /* GTMLightweightProxyTest.m in Sources */, 6294461C0EDE178D009295EA /* GTMNSArray+MergeTest.m in Sources */, 6294461D0EDE17A0009295EA /* GTMNSArray+Merge.m in Sources */, + 8B6C18740F3769D200E51E5D /* GTMNSObject+KeyValueObserving.m in Sources */, + 8B6C18750F3769D200E51E5D /* GTMNSObject+KeyValueObservingTest.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index d7e34b3..6c30b87 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -224,6 +224,14 @@ Changes since 1.5.1 - If the iPhone unittesting support is exiting when done, it now properly sets the exit code based on test success/failure. +- 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. + Release 1.5.1 Changes since 1.5.0 diff --git a/UnitTesting/GTMSenTestCase.m b/UnitTesting/GTMSenTestCase.m index dbd137a..8482c5a 100644 --- a/UnitTesting/GTMSenTestCase.m +++ b/UnitTesting/GTMSenTestCase.m @@ -281,6 +281,12 @@ NSString *const SenTestLineNumberKey = @"SenTestLineNumberKey"; - (void)tearDown { } + +- (NSString *)description { + // This matches the description OCUnit would return to you + return [NSString stringWithFormat:@"-[%@ %@]", [self class], + NSStringFromSelector(currentSelector_)]; +} @end #endif // GTM_IPHONE_SDK |