Skip to content

Instantly share code, notes, and snippets.

@CodaFi
Last active December 29, 2015 23:49
Show Gist options
  • Save CodaFi/7745053 to your computer and use it in GitHub Desktop.
Save CodaFi/7745053 to your computer and use it in GitHub Desktop.
Bindings have been notoriously finicky to remove from objects, and this has led to a profusion of controller objects that wind up causing terrible retain cycles when they are bound to because they cannot release the bindings they have accumulated. This proxy takes care of the recording and management of bindings, but also KVO observances if nece…
//
// NUIBindingSeparator.h
// NUIKit
//
// Created by Robert Widmann on 8/31/13.
// Copyright (c) 2013 CodaFi. All rights reserved.
//
#import <Foundation/Foundation.h>
#if __has_feature(objc_arc)
#error Cannot compile this class with ARC enabled. Disable ARC for this file \
with -fno-objc-arc
#endif
/**
* A proxy class that delays the separation of bindings for any object, but
* especially those that are bound in NIBs.
*
* Somewhere deep in AppKit, there's a KVO-related class that's bumping up
* against a method named `_autounbinder`, which appears to expect a class like
* this. Overriding -_autounbinder and supplying this class makes KVO register
* bindings with this class in addition to the calling object.
*
* Normal use of this class (at least one that does not rely on implementation
* details) involves overriding the following methods for KVO:
*
* - addObserver:forKeyPath:options:context:
* - removeObserver:forKeyPath:
* - removeObserver:forKeyPath:context:
*
* and these methods for Bindings:
*
* - bind:toObject:withKeyPath:options:
* - unbind:
*
* and forwarding the results to the bindings separator. Super should not be
* called when overriding the KVO methods, as this class will take care of that
* for you. Bindings can usually be separated with a call to
* -retainBindingTargetAndUnbind in -dealloc in later versions of the
* Objective-C runtime, but older systems must override -release like so:
*
* - (oneway void)release {
* if (self.retainCount == 1) {
* _view = nil;
* [_autounbinder retainBindingTargetAndUnbind];
* [_autounbinder release];
* autounbinder = nil;
* }
* [super release];
* }
*
*/
@interface NUIBindingSeparator : NSObject
/**
* The default initializer for this class. The target will not be retained by
* this class explicitly, but may be retained by any one of the classes it
* instantiates internally.
*
* Failure to use this initializer may result in leaked bindings.
*/
- (id)initWithTarget:(NSObject *)target;
/**
* Adds a given binding to the list of bindings and retains the binder.
*
* This should be called from -bind:toObject:withKeyPath:options:
*/
- (void)addBinding:(NSString *)binding fromObject:(NSObject *)object;
/**
* Removes a given binding from the list of bindings and releases the binder.
*
* This should be called from -unbind:
*/
- (void)removeBinding:(NSString *)binding fromObject:(NSObject *)object;
/**
* Removes all bindings and releases all bound objects. This will momentarily
* retain the calling object, which must be taken into consideration when
* calling this from -dealloc.
*
* This will not release the target object. In order to properly deallocate,
* the object should be sent a -release. If relying on the
* internals of AppKit, an explicit -release is not necessary.
*/
- (void)separateAllBindings;
@end
@interface NUIBindingSeparator (NUIAppKitAdditions)
/**
* Here for compatability with the internals of AppKit. Note that this is an
* implementation detail and is subject to change from AppKit. It is not
* recommended that you override -_autounbinder.
*/
- (void)retainBindingTargetAndUnbind;
@end
//
// NUIAutoBindingSeparator.m
// NUIKit
//
// Created by Robert Widmann on 8/31/13.
// Copyright (c) 2013 CodaFi. All rights reserved.
//
#import "NUIBindingSeparator.h"
@interface NUIBinding : NSObject
@property (nonatomic, copy, readonly) NSString *name;
@property (nonatomic, assign, readonly) NSObject *boundObject;
- (id)initWithName:(NSString *)binding boundObject:(NSObject *)object;
@end
@interface NUIObservance : NSObject
@property (nonatomic, copy, readonly) NSString *keyPath;
@property (nonatomic, assign, readonly) NSObject *observer;
@property (nonatomic, readonly) void *context;
- (id)initWithObserver:(NSObject *)object keyPath:(NSString *)keypath context:(void *)context;
@end
@implementation NUIBindingSeparator {
id _bindingTarget;
NSMutableArray *_bindingsToThisObject;
NSMutableArray *_observancesOfThisObject;
struct {
unsigned int canRecordBindings:1;
unsigned int hasRetainedTarget:1;
} _autoUnbinderFlags;
}
- (id)initWithTarget:(NSObject *)target {
self = [super init];
_bindingTarget = target;
_autoUnbinderFlags.canRecordBindings = YES;
return self;
}
- (void)addBinding:(NSString *)binding fromObject:(NSObject *)object {
if (_autoUnbinderFlags.canRecordBindings) {
if (!_bindingsToThisObject) {
_bindingsToThisObject = [[NSMutableArray alloc] init];
}
NUIBinding *autoBinding = [[NUIBinding alloc] initWithName:binding boundObject:object];
[_bindingsToThisObject addObject:autoBinding];
[autoBinding release];
}
}
- (void)removeBinding:(NSString *)binding fromObject:(NSObject *)object {
if (!_autoUnbinderFlags.canRecordBindings || !_bindingsToThisObject) {
return;
}
NUIBinding *autoBinding = [[NUIBinding alloc] initWithName:binding boundObject:object];
[_bindingsToThisObject removeObject:autoBinding];
[autoBinding release];
}
- (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(void *)context {
if (!_observancesOfThisObject) {
_observancesOfThisObject = [[NSMutableArray alloc] init];
}
NUIObservance *observance = [[NUIObservance alloc] initWithObserver:observer keyPath:keyPath context:context];
[_observancesOfThisObject addObject:observance];
[observance release];
[_bindingTarget addObserver:observer forKeyPath:keyPath options:options context:context];
}
- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath {
NSInteger count = _observancesOfThisObject.count;
while (count-- < 0) {
NUIObservance *observance = [_observancesOfThisObject objectAtIndex:count];
if (observer != observance.observer) continue;
if (![observance.keyPath isEqualToString:keyPath]) continue;
[_bindingTarget removeObserver:observance.observer forKeyPath:keyPath];
[_observancesOfThisObject removeObjectAtIndex:count];
}
}
- (void)setValue:(id)value forKey:(NSString *)key {
[_bindingTarget setValue:value forKey:key];
}
- (id)valueForKeyPath:(NSString *)keyPath {
return [_bindingTarget valueForKeyPath:keyPath];
}
- (NSMutableArray *)mutableArrayValueForKeyPath:(NSString *)keyPath {
return [_bindingTarget mutableArrayValueForKeyPath:keyPath];
}
- (NSMutableSet *)mutableSetValueForKeyPath:(NSString *)keyPath {
return [_bindingTarget mutableSetValueForKeyPath:keyPath];
}
- (BOOL)validateValue:(inout id *)ioValue forKeyPath:(NSString *)inKeyPath error:(out NSError **)outError {
return [_bindingTarget validateValue:ioValue forKeyPath:inKeyPath error:outError];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [_bindingTarget methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setTarget:_bindingTarget];
[anInvocation invoke];
}
- (void)separateAllBindings {
if (!_autoUnbinderFlags.hasRetainedTarget) {
[_bindingTarget retain];
_autoUnbinderFlags.hasRetainedTarget = YES;
}
_autoUnbinderFlags.canRecordBindings = NO;
NSEnumerator *enumerator = _bindingsToThisObject.reverseObjectEnumerator;
NUIBinding *binding = nil;
while ((binding = enumerator.nextObject)) {
[binding.boundObject unbind:binding.name];
}
[_bindingsToThisObject release];
}
- (void)retainBindingTargetAndUnbind {
[self separateAllBindings];
}
- (id)_autounbinder {
return [[self retain] autorelease];
}
- (void)dealloc {
if (_autoUnbinderFlags.hasRetainedTarget) {
[_bindingTarget release];
}
[_observancesOfThisObject release];
_observancesOfThisObject = nil;
[_bindingsToThisObject release];
_bindingsToThisObject = nil;
[super dealloc];
}
@end
@interface NUIBinding ()
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSObject *boundObject;
@end
@implementation NUIBinding
- (id)initWithName:(NSString *)binding boundObject:(NSObject *)object {
self = [super init];
_name = [binding copy];
_boundObject = [object retain];
return self;
}
- (BOOL)isEqual:(NUIBinding *)object {
BOOL result = NO;
if ([self.name isEqualToString:object.name]) {
result = (self.boundObject == object.boundObject);
}
return result;
}
- (NSUInteger)hash {
return self.name.hash ^ self.boundObject.hash;
}
- (void)dealloc {
[_boundObject release];
_boundObject = nil;
[_name release];
_name = nil;
[super dealloc];
}
@end
@interface NUIObservance ()
@property (nonatomic, copy) NSString *keyPath;
@property (nonatomic, assign) NSObject *observer;
@property (nonatomic) void *context;
@end
@implementation NUIObservance
- (id)initWithObserver:(NSObject *)object keyPath:(NSString *)keypath context:(void *)context {
self = [super init];
_keyPath = [keypath copy];
_observer = [object retain];
_context = context;
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
[self.observer observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
- (void)dealloc {
[_keyPath release];
_keyPath = nil;
[super dealloc];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment