Note that there are some explanatory texts on larger screens.

plurals
  1. POImplementing a KVO/Bindings-Compliant Bridge-Pattern in Cocoa
    text
    copied!<p>I'm trying to implement a simple object bridge in cocoa where the bridge object acts as a kvo/bindings-compliant drop in for some arbitrary other NSObject instance.</p> <p>Here is my problem (more details in the code below):</p> <p>A bridge object acts as a drop in for a Person-Object, with an NSString* property called <em>name</em> and an Address* property <em>address</em>. Binding to the keyPath "name" or "address" of the Bridge works nicely. Trouble starts when binding some object to the keyPath "address.street" of the bridge and a new Address-Object is set for Person's <em>address</em> property. That results in KVO-related exceptions that look like this:</p> <blockquote> <p><code>Cannot remove an observer &lt;NSKeyValueObservance 0x126b00&gt; for the key path "street" from &lt;Address 0x12f1d0&gt; because it is not registered as an observer</code></p> </blockquote> <p>This happens even though the bridge notices the change in the "address"-Property and emits a willChangeValueForKeyPath/didChangeValueForKeyPath tuple.</p> <p>The code below produces the the problem. It's self-contained objective-c code that can be saved in a file "BridgeDemo.m" and compiled run with</p> <pre><code>gcc -o test BridgeDemo.m -framework AppKit -framework Foundation; ./test </code></pre> <p>If you know a solution to this problem or can offer me a better approach solving the same problem you make me a <strong>very</strong> happy programmer!</p> <p>BridgeDemo.m:</p> <pre><code>#import &lt;Foundation/Foundation.h&gt; #import &lt;AppKit/AppKit.h&gt; /* --- Address ----------------------------------------- */ @interface Address : NSObject { NSString* street; NSNumber* zipCode; NSString* city; } @property(retain) NSString* street; @property(retain) NSNumber* zipCode; @property(retain) NSString* city; @end @implementation Address @synthesize street, zipCode, city; -(id)init { if( !( self = [super init] ) ) { return nil; } self.street = @"Elm Street"; self.zipCode = @"12345"; self.city = @"Crashington"; return self; } -(void) modifyStreet { self.street = @"Main Street"; } -(void)dealloc { [street release]; [zipCode release]; [city release]; [super dealloc]; } @end /* --- Person ----------------------------------------- */ @interface Person : NSObject { NSString* name; Address* address; } @property(retain) NSString* name; @property(retain) Address* address; @end @implementation Person @synthesize address, name; -(id)init { if( !( self = [super init] ) ) { return nil; } self.name = @"Tom"; self.address = [[Address new] autorelease]; return self; } - (void)modifyAddress { Address* a = [[Address new] autorelease]; a.street = @"Jump Street"; a.zipCode = @"54321"; a.city = @"Memleakville"; self.address = a; } - (void)dealloc { [address release]; [name release]; [super dealloc]; } @end /* --- Bridge ----------------------------------------- */ @interface Bridge : NSObject { NSMutableDictionary* observedKeys; NSObject* obj; } @property(retain) NSObject* obj; @end @implementation Bridge @synthesize obj; - (id)init { if( !( self = [super init] ) ) { return nil; } observedKeys = [NSMutableDictionary new]; return self; } - (void)forwardInvocation:(NSInvocation*)inv { [inv invokeWithTarget:obj]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { return [obj methodSignatureForSelector:aSelector]; } - (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSLog( @"&gt;&gt;&gt;&gt; Detected Change in keyPath: %@", keyPath ); [self willChangeValueForKey:keyPath]; [self didChangeValueForKey:keyPath]; } -(id)valueForUndefinedKey:(NSString*)key { /* Register an observer for the key, if not already done */ if( ![observedKeys objectForKey:key] ) { [observedKeys setObject:[NSNumber numberWithBool:YES] forKey:key]; [obj addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil]; } return [obj valueForKey:key]; } - (void)dealloc { for( NSString* key in [observedKeys allKeys] ) { [obj removeObserver:self forKeyPath:key]; } [obj release]; [observedKeys release]; [super dealloc]; } @end /* --- MyObserver ------------------------------------ */ @interface MyObserver : NSObject { Address* address; NSString* street; } @property(retain) Address* address; @property(retain) NSString* street; @end @implementation MyObserver @synthesize street, address; -(void)dealloc { [street release]; [super dealloc]; } @end /* This works fine */ void testBindingToAddress() { NSLog( @"Testing Binding to 'address' --------------" ); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Bridge* b = [[Bridge new] autorelease]; b.obj = [Person new]; MyObserver* o = [[MyObserver new] autorelease]; [o bind:@"address" toObject:b withKeyPath:@"address" options:nil]; NSLog( @"Before modifyStreet: %@", o.address.street ); [[b valueForKey:@"address"] performSelector:@selector(modifyStreet)]; NSLog( @"After modifyStreet: %@", o.address.street ); [b performSelector:@selector(modifyAddress)]; NSLog( @"After modifyAdress: %@", o.address.street ); [pool drain]; } /* This produces an exception */ void testBindingToStreet() { NSLog( @"Testing Binding to 'address.street' --------------" ); NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; Bridge* b = [[Bridge new] autorelease]; b.obj = [Person new]; MyObserver* o = [[MyObserver new] autorelease]; [o bind:@"street" toObject:b withKeyPath:@"address.street" options:nil]; NSLog( @"Before modifyStreet: %@", o.street ); [[b valueForKey:@"address"] performSelector:@selector(modifyStreet)]; NSLog( @"After modifyStreet: %@", o.street ); [b performSelector:@selector(modifyAddress)]; NSLog( @"After modifyAdress: %@", o.street ); [pool drain]; } /* --- main() ------------------------------------ */ int main (int argc, const char * argv[]) { testBindingToAddress(); testBindingToStreet(); return 0; } </code></pre>
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload