365 lines
12 KiB
Objective-C
365 lines
12 KiB
Objective-C
//
|
|
// NSObject+A2BlockDelegate.m
|
|
// BlocksKit
|
|
//
|
|
|
|
#import "NSObject+A2BlockDelegate.h"
|
|
@import ObjectiveC.message;
|
|
#import "A2DynamicDelegate.h"
|
|
#import "NSObject+A2DynamicDelegate.h"
|
|
|
|
#pragma mark - Declarations and macros
|
|
|
|
extern Protocol *a2_dataSourceProtocol(Class cls);
|
|
extern Protocol *a2_delegateProtocol(Class cls);
|
|
|
|
#pragma mark - Functions
|
|
|
|
static BOOL bk_object_isKindOfClass(id obj, Class testClass)
|
|
{
|
|
BOOL isKindOfClass = NO;
|
|
Class cls = object_getClass(obj);
|
|
while (cls && !isKindOfClass) {
|
|
isKindOfClass = (cls == testClass);
|
|
cls = class_getSuperclass(cls);
|
|
}
|
|
|
|
return isKindOfClass;
|
|
}
|
|
|
|
static Protocol *a2_protocolForDelegatingObject(id obj, Protocol *protocol)
|
|
{
|
|
NSString *protocolName = NSStringFromProtocol(protocol);
|
|
if ([protocolName hasSuffix:@"Delegate"]) {
|
|
Protocol *p = a2_delegateProtocol([obj class]);
|
|
if (p) return p;
|
|
} else if ([protocolName hasSuffix:@"DataSource"]) {
|
|
Protocol *p = a2_dataSourceProtocol([obj class]);
|
|
if (p) return p;
|
|
}
|
|
|
|
return protocol;
|
|
}
|
|
|
|
static inline BOOL isValidIMP(IMP impl) {
|
|
#if defined(__arm64__)
|
|
if (impl == NULL || impl == _objc_msgForward) return NO;
|
|
#else
|
|
if (impl == NULL || impl == _objc_msgForward || impl == (IMP)_objc_msgForward_stret) return NO;
|
|
#endif
|
|
return YES;
|
|
}
|
|
|
|
static BOOL addMethodWithIMP(Class cls, SEL oldSel, SEL newSel, IMP newIMP, const char *types, BOOL aggressive) {
|
|
if (!class_addMethod(cls, oldSel, newIMP, types)) {
|
|
return NO;
|
|
}
|
|
|
|
// We just ended up implementing a method that doesn't exist
|
|
// (-[NSURLConnection setDelegate:]) or overrode a superclass
|
|
// version (-[UIImagePickerController setDelegate:]).
|
|
IMP parentIMP = NULL;
|
|
Class superclass = class_getSuperclass(cls);
|
|
while (superclass && !isValidIMP(parentIMP)) {
|
|
parentIMP = class_getMethodImplementation(superclass, oldSel);
|
|
if (isValidIMP(parentIMP)) {
|
|
break;
|
|
} else {
|
|
parentIMP = NULL;
|
|
}
|
|
|
|
superclass = class_getSuperclass(superclass);
|
|
}
|
|
|
|
if (parentIMP) {
|
|
if (aggressive) {
|
|
return class_addMethod(cls, newSel, parentIMP, types);
|
|
}
|
|
|
|
class_replaceMethod(cls, newSel, newIMP, types);
|
|
class_replaceMethod(cls, oldSel, parentIMP, types);
|
|
}
|
|
|
|
return YES;
|
|
}
|
|
|
|
static BOOL swizzleWithIMP(Class cls, SEL oldSel, SEL newSel, IMP newIMP, const char *types, BOOL aggressive) {
|
|
Method origMethod = class_getInstanceMethod(cls, oldSel);
|
|
|
|
if (addMethodWithIMP(cls, oldSel, newSel, newIMP, types, aggressive)) {
|
|
return YES;
|
|
}
|
|
|
|
// common case, actual swap
|
|
BOOL ret = class_addMethod(cls, newSel, newIMP, types);
|
|
Method newMethod = class_getInstanceMethod(cls, newSel);
|
|
method_exchangeImplementations(origMethod, newMethod);
|
|
return ret;
|
|
}
|
|
|
|
static SEL selectorWithPattern(const char *prefix, const char *key, const char *suffix) {
|
|
size_t prefixLength = prefix ? strlen(prefix) : 0;
|
|
size_t suffixLength = suffix ? strlen(suffix) : 0;
|
|
|
|
char initial = key[0];
|
|
if (prefixLength) initial = (char)toupper(initial);
|
|
size_t initialLength = 1;
|
|
|
|
const char *rest = key + initialLength;
|
|
size_t restLength = strlen(rest);
|
|
|
|
char selector[prefixLength + initialLength + restLength + suffixLength + 1];
|
|
memcpy(selector, prefix, prefixLength);
|
|
selector[prefixLength] = initial;
|
|
memcpy(selector + prefixLength + initialLength, rest, restLength);
|
|
memcpy(selector + prefixLength + initialLength + restLength, suffix, suffixLength);
|
|
selector[prefixLength + initialLength + restLength + suffixLength] = '\0';
|
|
|
|
return sel_registerName(selector);
|
|
}
|
|
|
|
static SEL getterForProperty(objc_property_t property, const char *name)
|
|
{
|
|
if (property) {
|
|
char *getterName = property_copyAttributeValue(property, "G");
|
|
if (getterName) {
|
|
SEL getter = sel_getUid(getterName);
|
|
free(getterName);
|
|
if (getter) return getter;
|
|
}
|
|
}
|
|
|
|
const char *propertyName = property ? property_getName(property) : name;
|
|
return sel_registerName(propertyName);
|
|
}
|
|
|
|
static SEL setterForProperty(objc_property_t property, const char *name)
|
|
{
|
|
if (property) {
|
|
char *setterName = property_copyAttributeValue(property, "S");
|
|
if (setterName) {
|
|
SEL setter = sel_getUid(setterName);
|
|
free(setterName);
|
|
if (setter) return setter;
|
|
}
|
|
}
|
|
|
|
const char *propertyName = property ? property_getName(property) : name;
|
|
return selectorWithPattern("set", propertyName, ":");
|
|
}
|
|
|
|
static inline SEL prefixedSelector(SEL original) {
|
|
return selectorWithPattern("a2_", sel_getName(original), NULL);
|
|
}
|
|
|
|
#pragma mark -
|
|
|
|
typedef struct {
|
|
SEL setter;
|
|
SEL a2_setter;
|
|
SEL getter;
|
|
} A2BlockDelegateInfo;
|
|
|
|
static NSUInteger A2BlockDelegateInfoSize(const void *__unused item) {
|
|
return sizeof(A2BlockDelegateInfo);
|
|
}
|
|
|
|
static NSString *A2BlockDelegateInfoDescribe(const void *__unused item) {
|
|
if (!item) { return nil; }
|
|
const A2BlockDelegateInfo *info = item;
|
|
return [NSString stringWithFormat:@"(setter: %s, getter: %s)", sel_getName(info->setter), sel_getName(info->getter)];
|
|
}
|
|
|
|
static inline A2DynamicDelegate *getDynamicDelegate(NSObject *delegatingObject, Protocol *protocol, const A2BlockDelegateInfo *info, BOOL ensuring) {
|
|
A2DynamicDelegate *dynamicDelegate = [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)];
|
|
|
|
if (!info || !info->setter || !info->getter) {
|
|
return dynamicDelegate;
|
|
}
|
|
|
|
if (!info->a2_setter && !info->setter) { return dynamicDelegate; }
|
|
|
|
id (*getterDispatch)(id, SEL) = (id (*)(id, SEL)) objc_msgSend;
|
|
id originalDelegate = getterDispatch(delegatingObject, info->getter);
|
|
|
|
if (bk_object_isKindOfClass(originalDelegate, A2DynamicDelegate.class)) { return dynamicDelegate; }
|
|
|
|
void (*setterDispatch)(id, SEL, id) = (void (*)(id, SEL, id)) objc_msgSend;
|
|
setterDispatch(delegatingObject, info->a2_setter ?: info->setter, dynamicDelegate);
|
|
|
|
return dynamicDelegate;
|
|
}
|
|
|
|
typedef A2DynamicDelegate *(^A2GetDynamicDelegateBlock)(NSObject *, BOOL);
|
|
|
|
@interface A2DynamicDelegate ()
|
|
|
|
@property (nonatomic, weak, readwrite) id realDelegate;
|
|
|
|
@end
|
|
|
|
#pragma mark -
|
|
|
|
@implementation NSObject (A2BlockDelegate)
|
|
|
|
#pragma mark Helpers
|
|
|
|
+ (NSMapTable *)bk_delegateInfoByProtocol:(BOOL)createIfNeeded
|
|
{
|
|
NSMapTable *delegateInfo = objc_getAssociatedObject(self, _cmd);
|
|
if (delegateInfo || !createIfNeeded) { return delegateInfo; }
|
|
|
|
NSPointerFunctions *protocols = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsOpaqueMemory|NSPointerFunctionsObjectPointerPersonality];
|
|
NSPointerFunctions *infoStruct = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsMallocMemory|NSPointerFunctionsStructPersonality|NSPointerFunctionsCopyIn];
|
|
infoStruct.sizeFunction = A2BlockDelegateInfoSize;
|
|
infoStruct.descriptionFunction = A2BlockDelegateInfoDescribe;
|
|
|
|
delegateInfo = [[NSMapTable alloc] initWithKeyPointerFunctions:protocols valuePointerFunctions:infoStruct capacity:0];
|
|
objc_setAssociatedObject(self, _cmd, delegateInfo, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
|
|
|
|
return delegateInfo;
|
|
}
|
|
|
|
+ (const A2BlockDelegateInfo *)bk_delegateInfoForProtocol:(Protocol *)protocol
|
|
{
|
|
A2BlockDelegateInfo *infoAsPtr = NULL;
|
|
Class cls = self;
|
|
while ((infoAsPtr == NULL || infoAsPtr->getter == NULL) && cls != nil && cls != NSObject.class) {
|
|
NSMapTable *map = [cls bk_delegateInfoByProtocol:NO];
|
|
infoAsPtr = (__bridge void *)[map objectForKey:protocol];
|
|
cls = [cls superclass];
|
|
}
|
|
NSCAssert(infoAsPtr != NULL, @"Class %@ not assigned dynamic delegate for protocol %@", NSStringFromClass(self), NSStringFromProtocol(protocol));
|
|
return infoAsPtr;
|
|
}
|
|
|
|
#pragma mark Linking block properties
|
|
|
|
+ (void)bk_linkDataSourceMethods:(NSDictionary *)dictionary
|
|
{
|
|
[self bk_linkProtocol:a2_dataSourceProtocol(self) methods:dictionary];
|
|
}
|
|
|
|
+ (void)bk_linkDelegateMethods:(NSDictionary *)dictionary
|
|
{
|
|
[self bk_linkProtocol:a2_delegateProtocol(self) methods:dictionary];
|
|
}
|
|
|
|
+ (void)bk_linkProtocol:(Protocol *)protocol methods:(NSDictionary *)dictionary
|
|
{
|
|
[dictionary enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *selectorName, BOOL *stop) {
|
|
const char *name = propertyName.UTF8String;
|
|
objc_property_t property = class_getProperty(self, name);
|
|
NSCAssert(property, @"Property \"%@\" does not exist on class %s", propertyName, class_getName(self));
|
|
|
|
char *dynamic = property_copyAttributeValue(property, "D");
|
|
NSCAssert2(dynamic, @"Property \"%@\" on class %s must be backed with \"@dynamic\"", propertyName, class_getName(self));
|
|
free(dynamic);
|
|
|
|
char *copy = property_copyAttributeValue(property, "C");
|
|
NSCAssert2(copy, @"Property \"%@\" on class %s must be defined with the \"copy\" attribute", propertyName, class_getName(self));
|
|
free(copy);
|
|
|
|
SEL selector = NSSelectorFromString(selectorName);
|
|
SEL getter = getterForProperty(property, name);
|
|
SEL setter = setterForProperty(property, name);
|
|
|
|
if (class_respondsToSelector(self, setter) || class_respondsToSelector(self, getter)) { return; }
|
|
|
|
const A2BlockDelegateInfo *info = [self bk_delegateInfoForProtocol:protocol];
|
|
|
|
IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
|
|
A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, NO);
|
|
return [delegate blockImplementationForMethod:selector];
|
|
});
|
|
|
|
if (!class_addMethod(self, getter, getterImplementation, "@@:")) {
|
|
NSCAssert(NO, @"Could not implement getter for \"%@\" property.", propertyName);
|
|
}
|
|
|
|
IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id block) {
|
|
A2DynamicDelegate *delegate = getDynamicDelegate(delegatingObject, protocol, info, YES);
|
|
[delegate implementMethod:selector withBlock:block];
|
|
});
|
|
|
|
if (!class_addMethod(self, setter, setterImplementation, "v@:@")) {
|
|
NSCAssert(NO, @"Could not implement setter for \"%@\" property.", propertyName);
|
|
}
|
|
}];
|
|
}
|
|
|
|
#pragma mark Dynamic Delegate Replacement
|
|
|
|
+ (void)bk_registerDynamicDataSource
|
|
{
|
|
[self bk_registerDynamicDelegateNamed:@"dataSource" forProtocol:a2_dataSourceProtocol(self)];
|
|
}
|
|
+ (void)bk_registerDynamicDelegate
|
|
{
|
|
[self bk_registerDynamicDelegateNamed:@"delegate" forProtocol:a2_delegateProtocol(self)];
|
|
}
|
|
|
|
+ (void)bk_registerDynamicDataSourceNamed:(NSString *)dataSourceName
|
|
{
|
|
[self bk_registerDynamicDelegateNamed:dataSourceName forProtocol:a2_dataSourceProtocol(self)];
|
|
}
|
|
+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName
|
|
{
|
|
[self bk_registerDynamicDelegateNamed:delegateName forProtocol:a2_delegateProtocol(self)];
|
|
}
|
|
|
|
+ (void)bk_registerDynamicDelegateNamed:(NSString *)delegateName forProtocol:(Protocol *)protocol
|
|
{
|
|
NSMapTable *propertyMap = [self bk_delegateInfoByProtocol:YES];
|
|
A2BlockDelegateInfo *infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];
|
|
if (infoAsPtr != NULL) { return; }
|
|
|
|
const char *name = delegateName.UTF8String;
|
|
objc_property_t property = class_getProperty(self, name);
|
|
SEL setter = setterForProperty(property, name);
|
|
SEL a2_setter = prefixedSelector(setter);
|
|
SEL getter = getterForProperty(property, name);
|
|
|
|
A2BlockDelegateInfo info = {
|
|
setter, a2_setter, getter
|
|
};
|
|
|
|
[propertyMap setObject:(__bridge id)&info forKey:protocol];
|
|
infoAsPtr = (__bridge void *)[propertyMap objectForKey:protocol];
|
|
|
|
IMP setterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject, id delegate) {
|
|
A2DynamicDelegate *dynamicDelegate = getDynamicDelegate(delegatingObject, protocol, infoAsPtr, YES);
|
|
if ([delegate isEqual:dynamicDelegate]) {
|
|
delegate = nil;
|
|
}
|
|
dynamicDelegate.realDelegate = delegate;
|
|
});
|
|
|
|
if (!swizzleWithIMP(self, setter, a2_setter, setterImplementation, "v@:@", YES)) {
|
|
bzero(infoAsPtr, sizeof(A2BlockDelegateInfo));
|
|
return;
|
|
}
|
|
|
|
if (![self instancesRespondToSelector:getter]) {
|
|
IMP getterImplementation = imp_implementationWithBlock(^(NSObject *delegatingObject) {
|
|
return [delegatingObject bk_dynamicDelegateForProtocol:a2_protocolForDelegatingObject(delegatingObject, protocol)];
|
|
});
|
|
|
|
addMethodWithIMP(self, getter, NULL, getterImplementation, "@@:", NO);
|
|
}
|
|
}
|
|
|
|
- (id)bk_ensuredDynamicDelegate
|
|
{
|
|
Protocol *protocol = a2_delegateProtocol(self.class);
|
|
return [self bk_ensuredDynamicDelegateForProtocol:protocol];
|
|
}
|
|
|
|
- (id)bk_ensuredDynamicDelegateForProtocol:(Protocol *)protocol
|
|
{
|
|
const A2BlockDelegateInfo *info = [self.class bk_delegateInfoForProtocol:protocol];
|
|
return getDynamicDelegate(self, protocol, info, YES);
|
|
}
|
|
|
|
@end
|