From 93b7675bf61b4cfa9861bcfc0925b207f096db78 Mon Sep 17 00:00:00 2001 From: Sven Weidauer Date: Wed, 14 Nov 2018 18:33:07 +0100 Subject: [PATCH] Add SWBinder --- SWBinder.h | 108 ++++++++++++++++++++++++ SWBinder.m | 236 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 344 insertions(+) create mode 100644 SWBinder.h create mode 100644 SWBinder.m diff --git a/SWBinder.h b/SWBinder.h new file mode 100644 index 0000000..9b1f392 --- /dev/null +++ b/SWBinder.h @@ -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 diff --git a/SWBinder.m b/SWBinder.m new file mode 100644 index 0000000..fc72449 --- /dev/null +++ b/SWBinder.m @@ -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