Skip to content

Instantly share code, notes, and snippets.

@erichocean
Created September 15, 2008 00:02
Show Gist options
  • Select an option

  • Save erichocean/10781 to your computer and use it in GitHub Desktop.

Select an option

Save erichocean/10781 to your computer and use it in GitHub Desktop.
//
// SCBinding.h
// StateCode
//
// Created by Erich Ocean on 9/7/08.
// Copyright 2008 Erich Atlas Ocean. All rights reserved.
//
@interface SCBinding : NSObject
{
id from;
id to;
// set to true if you don't want changes to -> from to relay.
BOOL oneWay;
// set this to some value if you want a placeholder when the source
// value is an empty array or null (if you don't also set a nullPlaceholder)
id emptyPlaceholder;
// set this to some value if you want a placeholder value when the source
// value is null. If you don't set this but you do set an emptyPlaceholder
// then the emptyPlaceholder will be used instead.
id nullPlaceholder;
// set this to some value if you want a placeholder when the value is
// an array with multiple values. If you set this, arrays with single
// values will be converted to indivdual objects. If this is set ot null,
// multiple values will be allowed to pass untouched.
id multiplePlaceholder;
// set this to a function if you want a transform performed on an input
// value. This transform is performed both ways and before placeholder
// values are applied.
SEL transform; // should take three parameters: (BOOL) toIsRelayingValue, (NSString *) key, (id) value
SEL ext;
// private
BOOL _connected;
id _lastFromValue;
id _lastToValue;
}
+ binding: (NSDictionary *) config;
- (void) connect;
- (void) disconnect;
- (void) relay;
@end
@interface SCBindingFrom : SCBinding {} @end
@interface SCBindingNoChange : SCBindingFrom {} @end
@interface SCBindingNoError : SCBinding {} @end
@interface SCBindingNoErrorExt : SCBindingNoError {} @end
@interface SCBindingSingle : SCBinding {} @end
@interface SCBindingSingleNil : SCBinding {} @end
@interface SCBindingSingleNoError : SCBindingNoErrorExt {} @end
@interface SCBindingSingleNilNoError : SCBindingNoErrorExt {} @end
@interface SCBindingSingleNoChange : SCBindingNoChange {} @end // adds default SC placeholders (useful in views)
@interface SCBindingMultiple : SCBinding {} @end
@interface SCBindingMultipleNoError : SCBindingNoErrorExt {} @end
@interface SCBindingBool : SCBinding {} @end
@interface SCBindingBoolNoError : SCBindingNoErrorExt {} @end
@interface SCBindingNotNull : SCBinding {} @end
@interface SCBindingNotNullNoError : SCBindingNoErrorExt {} @end
@interface SCBindingNot : SCBinding {} @end
@interface SCBindingNotNoError : SCBindingNoErrorExt {} @end
@interface SCBindingIsNull : SCBinding {} @end
@interface SCBindingIsNullNoError : SCBindingNoErrorExt {} @end
//
// SCBinding.m
// StateCode
//
// Created by Erich Ocean on 9/7/08.
// Copyright 2008 Erich Atlas Ocean. All rights reserved.
//
#import "SCBinding.h"
char *incoming = "incoming";
char *outgoing = "outgoing";
NSMutableArray *_bindings; // prevent GC of bindings
@interface SCBinding (Private)
- (void) _fromObserverForTarget: (id) target key: (NSString *) key value: (id) value;
- (void) _toObserverForTarget: (id) target key: (NSString *) key value: (id) value;
- (BOOL) _didChangeFrom: (id) lastValue to: (id) newValue;
- (NSArray *) _walkTuple: (NSArray *) tuple;
@end
@interface NSObject (SCTruthiness)
- (BOOL) isTruthy;
@end
@implementation NSObject (SCTruthiness)
- (BOOL) isTruthy { return YES; }
@end
@implementation NSArray (SCTruthiness)
- (BOOL) isTruthy { return ( [self count] == 0 ) ? NO : YES; }
@end
@implementation NSDictionary (SCTruthiness)
- (BOOL) isTruthy { return ( [self count] == 0 ) ? NO : YES; }
@end
@implementation NSSet (SCTruthiness)
- (BOOL) isTruthy { return ( [self count] == 0 ) ? NO : YES; }
@end
@implementation NSString (SCTruthiness)
- (BOOL) isTruthy { return ( [self length] == 0 ) ? NO : YES; }
@end
@implementation NSNull (SCTruthiness)
- (BOOL) isTruthy { return NO; }
@end
@implementation NSNumber (SCTruthiness)
- (BOOL) isTruthy { return ( [self isEqualToNumber: [NSNumber numberWithInt: 0]] ) ? NO : YES; }
@end
@implementation SCBinding
+ (void) initialize
{
static BOOL initialized = NO;
if ( !initialized ) {
_bindings = [NSMutableArray array];
initialized = YES;
}
}
+ binding: (NSDictionary *) config
{
SCBinding *binding = [[self alloc] init];
for ( NSString *key in [config allKeys] ) {
if ( [key isEqualToString: @"transform"] ) {
binding->transform = NSSelectorFromString( [config objectForKey: key] );
}
else [binding setValue: [config objectForKey: key] forKey: key];
}
[_bindings addObject: binding];
[binding connect];
return binding;
}
- init
{
if ( self = [super init] ) {
from = nil;
to = nil;
oneWay = NO;
emptyPlaceholder = nil;
nullPlaceholder = nil;
multiplePlaceholder = nil;
transform = NULL;
ext = NULL;
_connected = NO;
_lastFromValue = nil;
_lastToValue = nil;
}
return self;
}
- (NSString *) description
{
return [NSString stringWithFormat: @"%@ from: %@ to: %@", [super description], from, to];
}
- (void) connect
{
if ( !_connected ) {
@try {
NSArray *tuple = [SC tupleForPropertyPath: from];
if ( tuple && (tuple = [self _walkTuple: tuple])) {
[[tuple objectAtIndex: 0]
addObserver: self
forKeyPath: [tuple objectAtIndex: 1]
options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew
context: (void *)incoming ];
}
if ( !oneWay ) {
tuple = [SC tupleForPropertyPath: to];
if ( tuple && (tuple = [self _walkTuple: tuple])) {
[[tuple objectAtIndex: 0]
addObserver: self
forKeyPath: [tuple objectAtIndex: 1]
options: NSKeyValueObservingOptionNew // don't care what value "to" starts with initially
context: (void *)outgoing ];
}
}
_connected = YES ;
}
@catch (id e) { NSLog( @"an exception occurred while connecting %@", self ); } // ignore
}
}
- (void) disconnect
{
if ( _connected ) {
_connected = NO;
NSArray *tuple = [SC tupleForPropertyPath: from];
if ( tuple && (tuple = [self _walkTuple: tuple])) {
[[tuple objectAtIndex: 0] removeObserver: self forKeyPath: [tuple objectAtIndex: 1]];
}
if ( !oneWay ) {
NSArray *tuple = [SC tupleForPropertyPath: to];
if ( tuple && (tuple = [self _walkTuple: tuple])) {
[[tuple objectAtIndex: 0] removeObserver: self forKeyPath: [tuple objectAtIndex: 1]];
}
}
}
}
// simulate a from -> to relay.
- (void) relay
{
NSArray *tuple = [SC tupleForPropertyPath: from];
if ( tuple && (tuple = [self _walkTuple: tuple])) {
[self _fromObserverForTarget: [tuple objectAtIndex: 0]
key: [tuple objectAtIndex: 1]
value: [[tuple objectAtIndex: 0] valueForKey: [tuple objectAtIndex: 1]] ];
}
}
// private
- (void) observeValueForKeyPath: (NSString *) keyPath ofObject: (id) object change: (NSDictionary *) change context: (void *) context
{
if ( context == incoming ) {
[self _fromObserverForTarget: object key: keyPath value: [change objectForKey: NSKeyValueChangeNewKey]];
}
else if ( context = outgoing ) {
[self _toObserverForTarget: object key: keyPath value: [change objectForKey: NSKeyValueChangeNewKey]];
}
else NSLog( @"observeValueForKeyPath not handled (should not happen)" );
}
- (void) _fromObserverForTarget: (id) target key: (NSString *) key value: (id) value
{
if ( value == [NSNull null] ) value = nil;
// no need to forward values if they haven't actually changed.
if ( ![self _didChangeFrom: _lastFromValue to: value ] ) return;
_lastFromValue = value ;
// try to get the to object.
NSArray *tuple = [SC tupleForPropertyPath: to];
if ( tuple && (tuple = [self _walkTuple: tuple]) ) {
// transform the value
if ( transform ) {
id (*transformImp)(id, SEL, BOOL, NSString *, id);
transformImp = (id (*)(id, SEL, BOOL, NSString *, id))[self methodForSelector: transform];
value = transformImp( self, transform, NO, key, value );
}
_lastToValue = value ;
// apply placeholder settings
if ( value ) {
if ( [value isKindOfClass: [NSArray class]] ) {
switch ( [value count] ) {
case 0:
value = ( emptyPlaceholder ) ? emptyPlaceholder : value;
break;
case 1:
value = [value objectAtIndex: 0];
break;
default:
value = ( multiplePlaceholder ) ? multiplePlaceholder : value;
break;
}
}
}
// now handle null placeholder
if ( !value ) value = ( nullPlaceholder ) ? nullPlaceholder : value;
if ( !value ) value = ( emptyPlaceholder ) ? emptyPlaceholder : value;
// send the value along to 'to'
[[tuple objectAtIndex: 0] setValue: value forKey: [tuple objectAtIndex: 1]];
}
}
- (void) _toObserverForTarget: (id) target key: (NSString *) key value: (id) value
{
if ( oneWay ) return; // don't propogate changes...
if ( value == [NSNull null] ) value = nil;
// no need to forward values if they haven't actually changed.
if ( ![self _didChangeFrom: _lastToValue to: value ] ) return;
_lastToValue = value ;
// try to get the to object.
NSArray *tuple = [SC tupleForPropertyPath: from];
if ( tuple && (tuple = [self _walkTuple: tuple]) ) {
// transform the value
if ( transform ) {
id (*transformImp)(id, SEL, BOOL, NSString *, id);
transformImp = (id (*)(id, SEL, BOOL, NSString *, id))[self methodForSelector: transform];
value = transformImp( self, transform, YES, key, value );
}
_lastFromValue = value ;
// send along to the 'from' source.
[[tuple objectAtIndex: 0] setValue: value forKey: [tuple objectAtIndex: 1]];
id result = [[tuple objectAtIndex: 0] valueForKey: [tuple objectAtIndex: 1]];
// Now that it has been set, the FROM object might not allow some
// changes. If that is the case, then we need to set this back on the sender.
if ( ![result isEqual: value] ) [target setValue: result forKey: key];
}
}
- (BOOL) _didChangeFrom: (id) lastValue to: (id) newValue
{
if ( newValue && lastValue ) return ![newValue isEqual: lastValue];
else if ( !newValue && !lastValue ) return NO ;
else return YES;
}
- (NSArray *) _walkTuple: (NSArray *) tuple
{
NSArray *parts = [[tuple objectAtIndex: 1] componentsSeparatedByString: @"."];
if ( parts.length > 1 )
{
id obj = [tuple objectAtIndex: 0];
NSString *key = [parts lastObject];
parts = [parts subarrayWithRange: NSMakeRange( 0, parts.length -1 )];
NSUInteger len = parts.length;
NSUInteger loc;
for ( NSUInteger loc = 0; obj && (loc < len); loc++ ) {
obj = [obj valueForKey: [parts objectAtIndex: loc]];
}
return ( loc < len || !obj )
? nil // hit a nil while walking the key path
: [NSArray arrayWithObjects: obj, key, nil];
}
else if ( parts.length == 1 ) return tuple; // already "walked"
else return nil; // can't be walked -- no key for object
}
- (id) _noErrorUp: (BOOL) yn key: (NSString *) key value: (id) value
{
return ( [value isKindOfClass: [NSError class]] ) ? nil : value ;
}
- (id) _noErrorExtUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( ext ) {
id (*transformImp)(id, SEL, BOOL, NSString *, id);
transformImp = (id (*)(id, SEL, BOOL, NSString *, id))[self methodForSelector: ext];
value = transformImp( self, ext, yn, key, value );
}
return [self _noErrorUp: yn key: key value: value];
}
- (id) _singleUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( [value isKindOfClass: [NSArray class]] ) {
switch ( [value count] ) {
case 0:
value = nil;
break;
case 1:
value = [value objectAtIndex: 0];
break;
default:
value = SC_MULTIPLE_PLACEHOLDER;
}
}
return value;
}
- (id) _singleNilUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( [value isKindOfClass: [NSArray class]] ) {
switch ( [value count] ) {
case 0:
value = nil;
break;
case 1:
value = [value objectAtIndex: 0];
break;
default:
value = nil;
}
}
return value;
}
// nil => [], x => [x], error => error, [x] => [x]
- (id) _multipleUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( !value ) value = [NSArray array];
else if ( ![value isKindOfClass: [NSArray class]] && ![value isKindOfClass: [NSError class]] ) value = [NSArray arrayWithObject: value];
return value;
}
- (id) _boolUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( !value ) return [NSNumber numberWithBool: NO];
else return [NSNumber numberWithBool: [value isTruthy]];
}
- (id) _notNullUp: (BOOL) yn key: (NSString *) key value: (id) value
{
return [NSNumber numberWithBool: (value) ? YES : NO];
}
- (id) _notUp: (BOOL) yn key: (NSString *) key value: (id) value
{
if ( !value ) return [NSNumber numberWithBool: YES];
else return [NSNumber numberWithBool: ![value isTruthy]];
}
- (id) _isNullUp: (BOOL) yn key: (NSString *) key value: (id) value
{
return [NSNumber numberWithBool: (value) ? NO : YES];
}
@end
@implementation SCBindingFrom
- init
{
if ( self = [super init] ) {
oneWay = YES;
}
return self;
}
@end
@implementation SCBindingNoChange @end // identical to SCBindingFrom
@implementation SCBindingNoError
- init
{
if ( self = [super init] ) {
transform = @selector(_noErrorUp:key:value:);
}
return self;
}
@end
@implementation SCBindingNoErrorExt
- init
{
if ( self = [super init] ) {
transform = @selector(_noErrorExtUp:key:value:);
}
return self;
}
@end
@implementation SCBindingSingle
- init
{
if ( self = [super init] ) {
transform = @selector(_singleUp:key:value:);
}
return self;
}
@end
@implementation SCBindingSingleNil
- init
{
if ( self = [super init] ) {
transform = @selector(_singleNilUp:key:value:);
}
return self;
}
@end
@implementation SCBindingSingleNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_singleUp:key:value:);
}
return self;
}
@end
@implementation SCBindingSingleNilNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_singleNilUp:key:value:);
}
return self;
}
@end
@implementation SCBindingSingleNoChange
- init
{
if ( self = [super init] ) {
multiplePlaceholder: SC_MULTIPLE_PLACEHOLDER;
emptyPlaceholder: SC_EMPTY_PLACEHOLDER;
nullPlaceholder: SC_NULL_PLACEHOLDER;
}
return self;
}
@end
@implementation SCBindingMultiple
- init
{
if ( self = [super init] ) {
transform = @selector(_multipleUp:key:value:);
}
return self;
}
@end
@implementation SCBindingMultipleNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_multipleUp:key:value:);
}
return self;
}
@end
@implementation SCBindingBool
- init
{
if ( self = [super init] ) {
transform = @selector(_boolUp:key:value:);
}
return self;
}
@end
@implementation SCBindingBoolNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_boolUp:key:value:);
}
return self;
}
@end
@implementation SCBindingNotNull
- init
{
if ( self = [super init] ) {
transform = @selector(_notNullUp:key:value:);
}
return self;
}
@end
@implementation SCBindingNotNullNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_notNullUp:key:value:);
}
return self;
}
@end
@implementation SCBindingNot
- init
{
if ( self = [super init] ) {
transform = @selector(_notUp:key:value:);
}
return self;
}
@end
@implementation SCBindingNotNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_notUp:key:value:);
}
return self;
}
@end
@implementation SCBindingIsNull
- init
{
if ( self = [super init] ) {
transform = @selector(_isNullUp:key:value:);
}
return self;
}
@end
@implementation SCBindingIsNullNoError
- init
{
if ( self = [super init] ) {
ext = @selector(_isNullUp:key:value:);
}
return self;
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment