516 lines
18 KiB
Objective-C
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);
|
|
}));
|
|
}
|
|
}
|