Add SWBinder
This commit is contained in:
parent
770e55e027
commit
93b7675bf6
2 changed files with 344 additions and 0 deletions
108
SWBinder.h
Normal file
108
SWBinder.h
Normal file
|
@ -0,0 +1,108 @@
|
|||
//
|
||||
// SWBinder.h
|
||||
//
|
||||
// Created by Sven Weidauer on 20.01.15.
|
||||
// Copyright (c) 2015 Sven Weidauer. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
/**
|
||||
* Block typed used to transform values.
|
||||
*
|
||||
* @param value The value to transform
|
||||
* @return The transformed value.
|
||||
*/
|
||||
typedef id (^SWBinderTransformationBlock)(id value);
|
||||
|
||||
/**
|
||||
* Establishes a binding from the source object to the target object using KVO.
|
||||
* Each time the value for a given key path of the source object is changed
|
||||
* the corresponding property of the target object is updated (again via a
|
||||
* key path).
|
||||
* Changed means that a different value is assigned, reassigning the current
|
||||
* value would not trigger an update of the target object.
|
||||
* This automatically handels all memory management. While both the source and
|
||||
* target objects are living it observes the source and updates the target. As
|
||||
* soon as either one gets deallocated the binder stops observing the source
|
||||
* and gets cleaned up. That means in most cases it is not necessary to keep
|
||||
* a reference to the Binder around.
|
||||
* You only need to keep a reference to the binder around if you want to stop
|
||||
* observing the source object before it or the target object gets deallocated.
|
||||
* This class is not meant to be subclassed.
|
||||
*/
|
||||
@interface SWBinder : NSObject
|
||||
|
||||
/**
|
||||
* Established a binding from a source to an target object.
|
||||
*
|
||||
* @param source The source object to observe
|
||||
* @param sourceKeyPath The key path of the source object to observe.
|
||||
* @param target The target object to update
|
||||
* @param targetKeyPath The keypath for the property of the target object
|
||||
* to update.
|
||||
* @return The new binder object.
|
||||
*/
|
||||
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
||||
toObject: (id)target keyPath: (NSString *)targetKeyPath;
|
||||
|
||||
/**
|
||||
* Established a binding from a source to an target object with an optional
|
||||
* transformation.
|
||||
*
|
||||
* @param source The source object to observe
|
||||
* @param sourceKeyPath The key path of the source object to observe.
|
||||
* @param target The target object to update
|
||||
* @param targetKeyPath The keypath for the property of the target object
|
||||
* to update.
|
||||
* @param block A block that can translate the source values to target values.
|
||||
* This may be @c nil if no translation is necessary.
|
||||
* @return The new binder object.
|
||||
*/
|
||||
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
||||
toObject: (id)target keyPath: (NSString *)targetKeyPath
|
||||
transformation: (SWBinderTransformationBlock)block;
|
||||
|
||||
/**
|
||||
* Established a binding from a source to an target object with a value
|
||||
* transformer.
|
||||
*
|
||||
* @param source The source object to observe
|
||||
* @param sourceKeyPath The key path of the source object to observe.
|
||||
* @param target The target object to update
|
||||
* @param targetKeyPath The keypath for the property of the target object
|
||||
* to update.
|
||||
* @param transformer A value transformer used to translate the values
|
||||
* from the source to target representation.
|
||||
* @return The new binder object.
|
||||
*/
|
||||
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
||||
toObject: (id)target keyPath: (NSString *)targetKeyPath
|
||||
valueTransformer: (NSValueTransformer *)transformer;
|
||||
|
||||
/**
|
||||
* Established a binding from a source to an target object with a value
|
||||
* transformer used in the reverse direction.
|
||||
*
|
||||
* @param source The source object to observe
|
||||
* @param sourceKeyPath The key path of the source object to observe.
|
||||
* @param target The target object to update
|
||||
* @param targetKeyPath The keypath for the property of the target object
|
||||
* to update.
|
||||
* @param transformer A value transformer used to translate the values
|
||||
* from the source to target representation. The transformer needs to
|
||||
* allow the reverse transformation.
|
||||
* @return The new binder object.
|
||||
*/
|
||||
+ (instancetype)bindFromObject: (id)source keyPath: (NSString *)sourceKeyPath
|
||||
toObject: (id)target keyPath: (NSString *)targetKeyPath
|
||||
reverseTransformer: (NSValueTransformer *)transformer;
|
||||
|
||||
/**
|
||||
* Breaks the binding. This removes the observer from the source object and
|
||||
* releases all resources. After this has been sent this Binder will never do
|
||||
* anything again and should be released.
|
||||
*/
|
||||
- (void)unbind;
|
||||
|
||||
@end
|
236
SWBinder.m
Normal file
236
SWBinder.m
Normal file
|
@ -0,0 +1,236 @@
|
|||
//
|
||||
// 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
|
Loading…
Add table
Reference in a new issue