Last active
May 16, 2020 02:51
-
-
Save davedelong/7371853 to your computer and use it in GitHub Desktop.
Delegate proxying (`protocol_methodForEach()` is a custom function that does just what its name implies)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@interface DDDelegateProxy : NSProxy | |
+ (id)proxyForDelegate:(id)delegate conformingToProtocol:(Protocol *)protocol; | |
@property (weak, readonly) id delegate; | |
@property (strong, readonly) Protocol *protocol; | |
/*! | |
* Set a default return value for a method on the delegate's protocol. | |
* \param value This must be a block that returns the default value. Bad Things will happen if it's not. | |
* \param selector The selector to which the default value is applied. | |
* This may be any selector in the delegate protocol \em or any of its conformed protocols. | |
*/ | |
- (void)setDefaultReturnValue:(id<NSCopying>)value forSelector:(SEL)selector; | |
@end | |
@implementation DDDelegateProxy { | |
id _defaultStub; | |
NSDictionary *_signatures; | |
NSDictionary *_types; | |
} | |
+ (id)proxyForDelegate:(id)delegate conformingToProtocol:(Protocol *)protocol { | |
return [[self alloc] initWithDelegate:delegate protocol:protocol]; | |
} | |
- (id)initWithDelegate:(id)delegate protocol:(Protocol *)protocol { | |
// create a new Class on which we'll hang the default values for this delegate | |
NSString *defaultStubName = [NSString stringWithFormat:@"DDDelegateDefaultStub_%s_%p", protocol_getName(protocol), delegate]; | |
Class defaultStubClass = NSClassFromString(defaultStubName); | |
if (defaultStubClass == nil) { | |
defaultStubClass = objc_allocateClassPair([NSObject class], [defaultStubName UTF8String], 0); | |
objc_registerClassPair(defaultStubClass); | |
} | |
_delegate = delegate; | |
_defaultStub = [[defaultStubClass alloc] init]; | |
_protocol = protocol; | |
// retrieve the instance methods for this protocol and save their info | |
NSMutableDictionary *sigs = [NSMutableDictionary dictionary]; | |
NSMutableDictionary *types = [NSMutableDictionary dictionary]; | |
protocol_methodForEach(protocol, YES, ^(SEL name, const char *signature, BOOL isInstanceMethod, BOOL isRequired, BOOL *stop) { | |
if (isInstanceMethod) { | |
NSString *n = NSStringFromSelector(name); | |
// we need to keep the signature ourselves because NSMethodSignature doesn't expose it. #15422834 | |
[types setObject:@(signature) forKey:n]; | |
[sigs setObject:[NSMethodSignature signatureWithObjCTypes:signature] forKey:n]; | |
} | |
}); | |
_signatures = [sigs copy]; | |
_types = [types copy]; | |
return self; | |
} | |
- (void)setDefaultReturnValue:(id<NSCopying>)value forSelector:(SEL)selector { | |
// turn the default value block into an IMP | |
// and hang the IMP off the default stub | |
NSString *name = NSStringFromSelector(selector); | |
NSString *type = [_types objectForKey:name]; | |
NSParameterAssert(value); | |
NSAssert(type != nil, @"Unable to find type information for selector: %@", name); | |
IMP i = imp_implementationWithBlock(value); | |
class_replaceMethod(object_getClass(_defaultStub), selector, i, [type UTF8String]); | |
} | |
- (id)forwardingTargetForSelector:(SEL)aSelector { | |
// fast-path method forwarding | |
id delegate = _delegate; | |
// prefer the delegate, obviously | |
if ([delegate respondsToSelector:aSelector]) { | |
return delegate; | |
} | |
// provide a default value | |
if ([_defaultStub respondsToSelector:aSelector]) { | |
return _defaultStub; | |
} | |
// fallback to slow path forwarding | |
return self; | |
} | |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { | |
return [_signatures objectForKey:NSStringFromSelector(sel)]; | |
} | |
- (void)forwardInvocation:(NSInvocation *)invocation { | |
// we'll simply return all zeros | |
NSMethodSignature *signature = [invocation methodSignature]; | |
NSUInteger length = [signature methodReturnLength]; | |
int8_t *zeroedReturnValue = calloc(length, sizeof(int8_t)); | |
[invocation setReturnValue:zeroedReturnValue]; | |
free(zeroedReturnValue); | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment