Created
September 15, 2008 00:02
-
-
Save erichocean/10781 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // | |
| // 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