stuff/SWBinder.m
2018-11-14 18:33:07 +01:00

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