Search code examples
objective-ccocoacocoa-bindings

Can you manually implement Cocoa bindings?


I've had a crack at implementing bindings for my own NSView subclass. It works, but there are problems with retain cycles when binding to File's Owner from a nib file. After reading into it a little, I discovered that Apple had the same problem a few years back but have fixed it with some magic undocumented class (NSAutounbinder).

There is a lengthy discussion of the retain cycle problem here http://www.cocoabuilder.com/archive/message/cocoa/2004/6/12/109600 . The workaround is to to unbind all bindings before the window controller is released, not before it is deallocated, in a place like windowWillClose:. This seems like an unnecessary hack to me.

My question is this: Is there any way to make custom bindings that work as well as the ones made by Apple, without using undocumented features? Am I going about this the wrong way?


UPDATE 2: I have found a solution that allows manually implemented bindings to work exactly like Apple's bindings. It takes advantage of the undocumented NSAutounbinder class, without actually using undocumented features. I will post the solution later today.


UPDATE: I've tried using exposeBinding:, and it doesn't seem to make any difference. However, the NSObject implementation of bind:toObject:withKeyPath:options: half works. It propogates changes from bindee to binder (i.e. from model/controller to view), but doesn't work the opposite way. Also, although the bindee is obviously being observed, observeValueForKeyPath:ofObject:change:context: is never triggered.

Example project here: http://www.tomdalling.com/wp-content/BindingsTest.zip

Apple's documentation indicates that you do, in fact, have to override bind:toObject:withKeyPath:options: to implement manual bindings. See here: http://developer.apple.com/documentation/Cocoa/Conceptual/CocoaBindings/Concepts/HowDoBindingsWork.html


SIDE NOTE: I've investigated how the undocumented NSAutounbinder works, and here's what I know.

When a binding is created to an NSWindowController, the bound object is actually an NSAutounbinder that is acquired from the NSWindowController with -[NSWindowController _autounbinder]. NSAutounbinder is a non-retaining proxy for the NSWindowController object. It is non-retaining to avoid the retain cycle problem.

When -[NSWindowController release] is called and retainCount == 1, The NSAutounbinder unbinds all bindings to itself. This ensures that there are no dangling pointers to the object before it is deallocated.


Solution

  • Here is the best solution I can find. I've got a more detailed discussion and demo code here: http://tomdalling.com/blog/cocoa/implementing-your-own-cocoa-bindings/

    Basically, you DO NOT override bind:toObject:withKeyPath:options: or unbind:. The default implementation on NSObject will use NSAutounbinder to avoid retain cycles. As Louis Gerbarg pointed out, there are still situations where NSAutounbinder doesn't kick in. However, you can get your bindings working at least as well as Apple's bindings.

    Because the default implementation of bind:toObject:withKeyPath:options: doesn't update the model when the view changes, view-driven changes must be propagated manually. You can use -[NSObject infoForBinding:] to get all the information necessary to update the bound object. I've added my own method on NSObject with a category:

    -(void)propagateValue:(id)value forBinding:(NSString*)binding;
    

    It handles getting the bound object, the bound key path, and applying the value transformer. The implementation is available from the link at the top.