236 lines
6.4 KiB
Objective-C
236 lines
6.4 KiB
Objective-C
//
|
|
// Binder.m
|
|
// Grind
|
|
//
|
|
// Created by Sven Weidauer on 20.01.15.
|
|
// Copyright (c) 2015 Sven Weidauer. All rights reserved.
|
|
//
|
|
|
|
#import "SWBinder.h"
|
|
|
|
@import ObjectiveC;
|
|
|
|
/**
|
|
* Helper function that adds a binder to an object so that it gets unbound
|
|
* before the object is deallocated.
|
|
*
|
|
* @param objct The object whose lifetime should be observed.
|
|
* @param biner The binder to unbind before @c object is deallocated.
|
|
*/
|
|
static inline void AddBinder( id object, SWBinder *binder );
|
|
|
|
/**
|
|
* Helper function that removes the lifetime observation from an object.
|
|
*
|
|
* @param object The object whose lifetime is observed
|
|
* @param binder The binder that is observing the object.
|
|
*/
|
|
static inline void RemoveBinder( id object, SWBinder *binder );
|
|
|
|
@implementation SWBinder {
|
|
// Normally I wouldn't use instance variables directly as I do here.
|
|
// It's OK here since this is not supposed to be subclassed and the
|
|
// binder objects should be considered immutable.
|
|
|
|
// __unsafe_unretained so that we still have this reference while the
|
|
// object is being deallocated so we can remove the observer.
|
|
__unsafe_unretained id _source;
|
|
NSString *_sourceKeyPath;
|
|
|
|
__unsafe_unretained id _target;
|
|
NSString *_targetKeyPath;
|
|
|
|
SWBinderTransformationBlock _transform;
|
|
}
|
|
|
|
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
|
toObject: (id)target keyPath: (NSString *)targetKeyPath
|
|
valueTransformer: (NSValueTransformer *)transformer;
|
|
{
|
|
NSParameterAssert( transformer );
|
|
|
|
SWBinderTransformationBlock block = ^( id value ) {
|
|
return [transformer transformedValue: value];
|
|
};
|
|
|
|
return [self bindFromObject: source keyPath: sourceKeyPath
|
|
toObject: target keyPath: targetKeyPath
|
|
transformation: block];
|
|
}
|
|
|
|
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
|
toObject: (id)target keyPath: (NSString *)targetKeyPath
|
|
reverseTransformer: (NSValueTransformer *)transformer;
|
|
{
|
|
NSParameterAssert( transformer );
|
|
NSAssert( [transformer.class allowsReverseTransformation], @"Reverse transformation needed for %@", NSStringFromSelector( _cmd ) );
|
|
|
|
SWBinderTransformationBlock block = ^( id value ) {
|
|
return [transformer reverseTransformedValue: value];
|
|
};
|
|
|
|
return [self bindFromObject: source keyPath: sourceKeyPath
|
|
toObject: target keyPath: targetKeyPath
|
|
transformation: block];
|
|
}
|
|
|
|
|
|
+ (instancetype)bindFromObject:(id)source keyPath:(NSString *)sourceKeyPath
|
|
toObject:(id)target keyPath:(NSString *)targetKeyPath
|
|
{
|
|
return [self bindFromObject: source keyPath: sourceKeyPath
|
|
toObject: target keyPath: targetKeyPath
|
|
transformation: nil];
|
|
}
|
|
|
|
+ (instancetype)bindFromObject:(id)source keyPath:(NSString *)sourceKeyPath
|
|
toObject:(id)target keyPath:(NSString *)targetKeyPath
|
|
transformation:(SWBinderTransformationBlock)block
|
|
{
|
|
return [[self alloc] initWithSource: source keyPath: sourceKeyPath
|
|
target: target keyPath: targetKeyPath
|
|
transformation: block];
|
|
}
|
|
|
|
- (instancetype)initWithSource:(id)source keyPath:(NSString *)sourceKeyPath
|
|
target:(id)target keyPath:(NSString *)targetKeyPath
|
|
transformation:(SWBinderTransformationBlock)block;
|
|
{
|
|
NSParameterAssert( source && sourceKeyPath && target && targetKeyPath );
|
|
|
|
self = [super init];
|
|
if (!self) return nil;
|
|
|
|
_source = source;
|
|
_sourceKeyPath = [sourceKeyPath copy];
|
|
|
|
_target = target;
|
|
_targetKeyPath = [targetKeyPath copy];
|
|
|
|
_transform = [block copy];
|
|
|
|
[source addObserver: self forKeyPath: _sourceKeyPath
|
|
options: NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
|
|
context: NULL];
|
|
|
|
AddBinder( _target, self );
|
|
AddBinder( _source, self );
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
|
|
change:(NSDictionary *)change context:(void *)context
|
|
{
|
|
id target = _target;
|
|
|
|
id newValue = change[NSKeyValueChangeNewKey];
|
|
id previousValue = change[NSKeyValueChangeOldKey];
|
|
|
|
if (newValue == previousValue || [newValue isEqual: previousValue]) {
|
|
// The value did not change, so we won't update the target. This
|
|
// prevents endless cycles when doing bidirectional bindings.
|
|
return;
|
|
}
|
|
|
|
if ([newValue isEqual: [NSNull null]]) {
|
|
newValue = nil;
|
|
}
|
|
|
|
if (_transform) {
|
|
newValue = _transform( newValue );
|
|
}
|
|
|
|
[target setValue: newValue forKeyPath: _targetKeyPath];
|
|
}
|
|
|
|
- (void)unbind
|
|
{
|
|
id source = _source;
|
|
if (source) {
|
|
_source = nil;
|
|
|
|
[source removeObserver: self forKeyPath: _sourceKeyPath];
|
|
|
|
RemoveBinder( source, self );
|
|
}
|
|
|
|
_sourceKeyPath = nil;
|
|
|
|
id target = _target;
|
|
if (target) {
|
|
_target = nil;
|
|
|
|
RemoveBinder( target, self );
|
|
}
|
|
|
|
_targetKeyPath = nil;
|
|
|
|
_transform = nil;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[self unbind];
|
|
}
|
|
|
|
@end
|
|
|
|
|
|
/**
|
|
* Helper object. Unbinds a binder in its dealloc method. Used for observing
|
|
* the lifetime of source and target objects.
|
|
*/
|
|
@interface SWBinderAutoRemoveHelper_ : NSObject
|
|
|
|
/**
|
|
* Designated initializer.
|
|
* @param binder The binder to unbind in dealloc.
|
|
*/
|
|
- (instancetype)initWithBinder:(SWBinder *)binder NS_DESIGNATED_INITIALIZER;
|
|
|
|
/**
|
|
* Removes the reference to the binder.
|
|
*/
|
|
- (void)stop;
|
|
|
|
@end
|
|
|
|
static inline void AddBinder( id object, SWBinder *binder )
|
|
{
|
|
objc_setAssociatedObject( object, (__bridge const void *)binder, [[SWBinderAutoRemoveHelper_ alloc] initWithBinder: binder], OBJC_ASSOCIATION_RETAIN_NONATOMIC );
|
|
}
|
|
|
|
static inline void RemoveBinder( id object, SWBinder *binder )
|
|
{
|
|
[(SWBinderAutoRemoveHelper_ *)objc_getAssociatedObject( object, (__bridge const void *)binder ) stop];
|
|
objc_setAssociatedObject( object, (__bridge const void *)binder, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC );
|
|
}
|
|
|
|
@implementation SWBinderAutoRemoveHelper_ {
|
|
SWBinder *_binder;
|
|
}
|
|
|
|
- (instancetype)initWithBinder:(SWBinder *)binder;
|
|
{
|
|
NSParameterAssert( binder );
|
|
|
|
self = [super init];
|
|
if (!self) return nil;
|
|
|
|
_binder = binder;
|
|
|
|
return self;
|
|
}
|
|
|
|
- (void)dealloc
|
|
{
|
|
[_binder unbind];
|
|
}
|
|
|
|
- (void)stop
|
|
{
|
|
_binder = nil;
|
|
}
|
|
|
|
@end
|