2016-04-13 16:13:51 +02:00

516 lines
18 KiB
Objective-C

//
// NSObject+RZDataBinding.m
//
// Created by Rob Visentin on 9/17/14.
// Copyright 2014 Raizlabs and other contributors
// http://raizlabs.com/
//
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
#import <objc/runtime.h>
#import <objc/message.h>
#import "NSObject+RZDataBinding.h"
#import "RZDBMacros.h"
@class RZDBObserver;
@class RZDBObserverContainer;
// public change keys
NSString* const kRZDBChangeKeyObject = @"RZDBChangeObject";
NSString* const kRZDBChangeKeyOld = @"RZDBChangeOld";
NSString* const kRZDBChangeKeyNew = @"RZDBChangeNew";
NSString* const kRZDBChangeKeyKeyPath = @"RZDBChangeKeyPath";
// private change keys
static NSString* const kRZDBChangeKeyBoundKey = @"_RZDBChangeBoundKey";
static NSString* const kRZDBChangeKeyBindingTransformKey = @"_RZDBChangeBindingTransform";
static void* const kRZDBSwizzledDeallocKey = (void *)&kRZDBSwizzledDeallocKey;
static void* const kRZDBKVOContext = (void *)&kRZDBKVOContext;
#define RZDBNotNull(obj) ((obj) != nil && ![(obj) isEqual:[NSNull null]])
#pragma mark - RZDataBinding_Private interface
// methods used to implement RZDB_AUTOMATIC_CLEANUP
BOOL rz_requiresDeallocSwizzle(Class class);
void rz_swizzleDeallocIfNeeded(Class class);
@interface NSObject (RZDataBinding_Private)
- (NSMutableArray *)rz_registeredObservers;
- (void)rz_setRegisteredObservers:(NSMutableArray *)registeredObservers;
- (RZDBObserverContainer *)rz_dependentObservers;
- (void)rz_setDependentObservers:(RZDBObserverContainer *)dependentObservers;
- (void)rz_addTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey bindingTransform:(RZDBKeyBindingTransform)bindingTransform forKeyPath:(NSString *)keyPath withOptions:(NSKeyValueObservingOptions)options;
- (void)rz_removeTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey forKeyPath:(NSString *)keyPath;
- (void)rz_observeBoundKeyChange:(NSDictionary *)change;
- (void)rz_setBoundKey:(NSString *)key withValue:(id)value transform:(RZDBKeyBindingTransform)transform;
@end
#pragma mark - RZDBObserver interface
@interface RZDBObserver : NSObject;
@property (assign, nonatomic) __unsafe_unretained NSObject *observedObject;
@property (copy, nonatomic) NSString *keyPath;
@property (copy, nonatomic) NSString *boundKey;
@property (assign, nonatomic) NSKeyValueObservingOptions observationOptions;
@property (assign, nonatomic) __unsafe_unretained id target;
@property (assign, nonatomic) SEL action;
@property (strong, nonatomic) NSMethodSignature *methodSignature;
@property (copy, nonatomic) RZDBKeyBindingTransform bindingTransform;
- (instancetype)initWithObservedObject:(NSObject *)observedObject keyPath:(NSString *)keyPath observationOptions:(NSKeyValueObservingOptions)observingOptions;
- (void)setTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey bindingTransform:(RZDBKeyBindingTransform)bindingTransform;
- (void)invalidate;
@end
#pragma mark - RZDBObserverContainer interface
@interface RZDBObserverContainer : NSObject
@property (strong, nonatomic) NSHashTable *observers;
- (void)addObserver:(RZDBObserver *)observer;
- (void)removeObserver:(RZDBObserver *)observer;
@end
#pragma mark - RZDataBinding implementation
@implementation NSObject (RZDataBinding)
- (void)rz_addTarget:(id)target action:(SEL)action forKeyPathChange:(NSString *)keyPath
{
[self rz_addTarget:target action:action forKeyPathChange:keyPath callImmediately:NO];
}
- (void)rz_addTarget:(id)target action:(SEL)action forKeyPathChange:(NSString *)keyPath callImmediately:(BOOL)callImmediately
{
NSParameterAssert(target);
NSParameterAssert(action);
NSKeyValueObservingOptions observationOptions = kNilOptions;
if ( [target methodSignatureForSelector:action].numberOfArguments > 2 ) {
observationOptions |= NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld;
}
if ( callImmediately ) {
observationOptions |= NSKeyValueObservingOptionInitial;
}
[self rz_addTarget:target action:action boundKey:nil bindingTransform:nil forKeyPath:keyPath withOptions:observationOptions];
}
- (void)rz_addTarget:(id)target action:(SEL)action forKeyPathChanges:(NSArray *)keyPaths
{
[self rz_addTarget:target action:action forKeyPathChanges:keyPaths callImmediately:NO];
}
- (void)rz_addTarget:(id)target action:(SEL)action forKeyPathChanges:(NSArray *)keyPaths callImmediately:(BOOL)callImmediately
{
BOOL callMultiple = NO;
if ( callImmediately ) {
callMultiple = [target methodSignatureForSelector:action].numberOfArguments > 2;
}
[keyPaths enumerateObjectsUsingBlock:^(NSString *keyPath, NSUInteger idx, BOOL *stop) {
[self rz_addTarget:target action:action forKeyPathChange:keyPath callImmediately:callMultiple];
}];
if ( callImmediately && !callMultiple ) {
((void(*)(id, SEL))objc_msgSend)(target, action);
}
}
- (void)rz_removeTarget:(id)target action:(SEL)action forKeyPathChange:(NSString *)keyPath
{
[self rz_removeTarget:target action:action boundKey:nil forKeyPath:keyPath];
}
- (void)rz_bindKey:(NSString *)key toKeyPath:(NSString *)foreignKeyPath ofObject:(id)object
{
[self rz_bindKey:key toKeyPath:foreignKeyPath ofObject:object withTransform:nil];
}
- (void)rz_bindKey:(NSString *)key toKeyPath:(NSString *)foreignKeyPath ofObject:(id)object withTransform:(RZDBKeyBindingTransform)bindingTransform
{
NSParameterAssert(key);
NSParameterAssert(foreignKeyPath);
if ( object != nil ) {
@try {
id val = [object valueForKeyPath:foreignKeyPath];
[self rz_setBoundKey:key withValue:val transform:bindingTransform];
}
@catch (NSException *exception) {
[NSException raise:NSInvalidArgumentException format:@"RZDataBinding cannot bind key:%@ to key path:%@ of object:%@. Reason: %@", key, foreignKeyPath, [object description], exception.reason];
}
[object rz_addTarget:self action:@selector(rz_observeBoundKeyChange:) boundKey:key bindingTransform:bindingTransform forKeyPath:foreignKeyPath withOptions:NSKeyValueObservingOptionNew];
}
}
- (void)rz_unbindKey:(NSString *)key fromKeyPath:(NSString *)foreignKeyPath ofObject:(id)object
{
[object rz_removeTarget:self action:@selector(rz_observeBoundKeyChange:) boundKey:key forKeyPath:foreignKeyPath];
}
@end
#pragma mark - RZDataBinding_Private implementation
@implementation NSObject (RZDataBinding_Private)
- (NSMutableArray *)rz_registeredObservers
{
return objc_getAssociatedObject(self, @selector(rz_registeredObservers));
}
- (void)rz_setRegisteredObservers:(NSMutableArray *)registeredObservers
{
objc_setAssociatedObject(self, @selector(rz_registeredObservers), registeredObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (RZDBObserverContainer *)rz_dependentObservers
{
return objc_getAssociatedObject(self, @selector(rz_dependentObservers));
}
- (void)rz_setDependentObservers:(RZDBObserverContainer *)dependentObservers
{
objc_setAssociatedObject(self, @selector(rz_dependentObservers), dependentObservers, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (void)rz_addTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey bindingTransform:(RZDBKeyBindingTransform)bindingTransform forKeyPath:(NSString *)keyPath withOptions:(NSKeyValueObservingOptions)options
{
NSMutableArray *registeredObservers = nil;
RZDBObserverContainer *dependentObservers = nil;
RZDBObserver *observer = [[RZDBObserver alloc] initWithObservedObject:self keyPath:keyPath observationOptions:options];
[observer setTarget:target action:action boundKey:boundKey bindingTransform:bindingTransform];
@synchronized (self) {
registeredObservers = [self rz_registeredObservers];
if ( registeredObservers == nil ) {
registeredObservers = [NSMutableArray array];
[self rz_setRegisteredObservers:registeredObservers];
}
[registeredObservers addObject:observer];
}
@synchronized (target) {
dependentObservers = [target rz_dependentObservers];
if ( dependentObservers == nil ) {
dependentObservers = [[RZDBObserverContainer alloc] init];
[target rz_setDependentObservers:dependentObservers];
}
[dependentObservers addObserver:observer];
}
#if RZDB_AUTOMATIC_CLEANUP
rz_swizzleDeallocIfNeeded([self class]);
rz_swizzleDeallocIfNeeded([target class]);
#endif
}
- (void)rz_removeTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey forKeyPath:(NSString *)keyPath
{
@synchronized (self) {
NSMutableArray *registeredObservers = [self rz_registeredObservers];
[registeredObservers enumerateObjectsUsingBlock:^(RZDBObserver *observer, NSUInteger idx, BOOL *stop) {
BOOL targetsEqual = (target == observer.target);
BOOL actionsEqual = (action == NULL || action == observer.action);
BOOL boundKeysEqual = (boundKey == observer.boundKey || [boundKey isEqualToString:observer.boundKey]);
BOOL keyPathsEqual = [keyPath isEqualToString:observer.keyPath];
BOOL allEqual = (targetsEqual && actionsEqual && boundKeysEqual && keyPathsEqual);
if ( allEqual ) {
[observer invalidate];
}
}];
}
}
- (void)rz_observeBoundKeyChange:(NSDictionary *)change
{
NSString *boundKey = change[kRZDBChangeKeyBoundKey];
if ( boundKey != nil ) {
id value = change[kRZDBChangeKeyNew];
[self rz_setBoundKey:boundKey withValue:value transform:change[kRZDBChangeKeyBindingTransformKey]];
}
}
- (void)rz_setBoundKey:(NSString *)key withValue:(id)value transform:(RZDBKeyBindingTransform)transform
{
id currentValue = [self valueForKey:key];
if ( transform != nil ) {
value = transform(value);
}
if ( currentValue != value && ![currentValue isEqual:value] ) {
[self setValue:value forKey:key];
}
}
- (void)rz_cleanupObservers
{
NSMutableArray *registeredObservers = [self rz_registeredObservers];
RZDBObserverContainer *dependentObservers = [self rz_dependentObservers];
[[registeredObservers copy] enumerateObjectsUsingBlock:^(RZDBObserver *obs, NSUInteger idx, BOOL *stop) {
[obs invalidate];
}];
[[dependentObservers.observers allObjects] enumerateObjectsUsingBlock:^(RZDBObserver *obs, NSUInteger idx, BOOL *stop) {
[obs invalidate];
}];
}
@end
#pragma mark - RZDBObserver implementation
@implementation RZDBObserver
- (instancetype)initWithObservedObject:(NSObject *)observedObject keyPath:(NSString *)keyPath observationOptions:(NSKeyValueObservingOptions)observingOptions
{
self = [super init];
if ( self != nil ) {
_observedObject = observedObject;
_keyPath = keyPath;
_observationOptions = observingOptions;
}
return self;
}
- (void)setTarget:(id)target action:(SEL)action boundKey:(NSString *)boundKey bindingTransform:(RZDBKeyBindingTransform)bindingTransform
{
self.target = target;
self.action = action;
self.methodSignature = [target methodSignatureForSelector:action];
self.boundKey = boundKey;
self.bindingTransform = bindingTransform;
[self.observedObject addObserver:self forKeyPath:self.keyPath options:self.observationOptions context:kRZDBKVOContext];
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ( context == kRZDBKVOContext ) {
if ( self.methodSignature.numberOfArguments > 2 ) {
NSDictionary *changeDict = [self changeDictForKVOChange:change];
((void(*)(id, SEL, NSDictionary *))objc_msgSend)(self.target, self.action, changeDict);
}
else {
((void(*)(id, SEL))objc_msgSend)(self.target, self.action);
}
}
}
- (NSDictionary *)changeDictForKVOChange:(NSDictionary *)kvoChange
{
NSMutableDictionary *changeDict = [NSMutableDictionary dictionary];
if ( self.observedObject != nil ) {
changeDict[kRZDBChangeKeyObject] = self.observedObject;
}
if ( RZDBNotNull(kvoChange[NSKeyValueChangeOldKey]) ) {
changeDict[kRZDBChangeKeyOld] = kvoChange[NSKeyValueChangeOldKey];
}
if ( RZDBNotNull(kvoChange[NSKeyValueChangeNewKey]) ) {
changeDict[kRZDBChangeKeyNew] = kvoChange[NSKeyValueChangeNewKey];
}
if ( self.keyPath != nil ) {
changeDict[kRZDBChangeKeyKeyPath] = self.keyPath;
}
if ( self.boundKey != nil ) {
changeDict[kRZDBChangeKeyBoundKey] = self.boundKey;
}
if ( self.bindingTransform != nil ) {
changeDict[kRZDBChangeKeyBindingTransformKey] = self.bindingTransform;
}
return [changeDict copy];
}
- (void)invalidate
{
[[self.target rz_dependentObservers] removeObserver:self];
[[self.observedObject rz_registeredObservers] removeObject:self];
// KVO throws an exception when removing an observer that was never added.
// This should never be a problem given how things are setup, but make sure to avoid a crash.
@try {
[self.observedObject removeObserver:self forKeyPath:self.keyPath context:kRZDBKVOContext];
}
@catch (__unused NSException *exception) {
RZDBLog(@"RZDataBinding attempted to remove an observer from object:%@, but the observer was never added. This shouldn't have happened, but won't affect anything going forward.", self.observedObject);
}
self.observedObject = nil;
self.target = nil;
self.action = NULL;
self.methodSignature = nil;
}
@end
#pragma mark - RZDBObserverContainer implementation
@implementation RZDBObserverContainer
- (instancetype)init
{
self = [super init];
if ( self != nil ) {
_observers = [NSHashTable weakObjectsHashTable];
}
return self;
}
- (void)addObserver:(RZDBObserver *)observer
{
@synchronized (self) {
[self.observers addObject:observer];
}
}
- (void)removeObserver:(RZDBObserver *)observer
{
@synchronized (self) {
[self.observers removeObject:observer];
}
}
@end
// a class doesn't need dealloc swizzled if it or a superclass has been swizzled already
BOOL rz_requiresDeallocSwizzle(Class class)
{
BOOL swizzled = NO;
for ( Class currentClass = class; !swizzled && currentClass != nil; currentClass = class_getSuperclass(currentClass) ) {
swizzled = [objc_getAssociatedObject(currentClass, kRZDBSwizzledDeallocKey) boolValue];
}
return !swizzled;
}
// In order for automatic cleanup to work, observers must be removed before deallocation.
// This method ensures that rz_cleanupObservers is called in the dealloc of classes of objects
// that are used in RZDataBinding.
void rz_swizzleDeallocIfNeeded(Class class)
{
static SEL deallocSEL = NULL;
static SEL cleanupSEL = NULL;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
deallocSEL = sel_getUid("dealloc");
cleanupSEL = sel_getUid("rz_cleanupObservers");
});
@synchronized (class) {
if ( !rz_requiresDeallocSwizzle(class) ) {
// dealloc swizzling already resolved
return;
}
objc_setAssociatedObject(class, kRZDBSwizzledDeallocKey, @(YES), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
Method dealloc = NULL;
// search instance methods of the class (does not search superclass methods)
unsigned int n;
Method *methods = class_copyMethodList(class, &n);
for ( unsigned int i = 0; i < n; i++ ) {
if ( method_getName(methods[i]) == deallocSEL ) {
dealloc = methods[i];
break;
}
}
free(methods);
if ( dealloc == NULL ) {
Class superclass = class_getSuperclass(class);
// class does not implement dealloc, so implement it directly
class_addMethod(class, deallocSEL, imp_implementationWithBlock(^(__unsafe_unretained id self) {
// cleanup RZDB observers
((void(*)(id, SEL))objc_msgSend)(self, cleanupSEL);
// ARC automatically calls super when dealloc is implemented in code,
// but when provided our own dealloc IMP we have to call through to super manually
struct objc_super superStruct = (struct objc_super){ self, superclass };
((void (*)(struct objc_super*, SEL))objc_msgSendSuper)(&superStruct, deallocSEL);
}), method_getTypeEncoding(dealloc));
}
else {
// class implements dealloc, so extend the existing implementation
__block IMP deallocIMP = method_setImplementation(dealloc, imp_implementationWithBlock(^(__unsafe_unretained id self) {
// cleanup RZDB observers
((void(*)(id, SEL))objc_msgSend)(self, cleanupSEL);
// invoke the original dealloc IMP
((void(*)(id, SEL))deallocIMP)(self, deallocSEL);
}));
}
}