Skip to content

Instantly share code, notes, and snippets.

@davedelong
Last active May 16, 2020 02:51
Show Gist options
  • Save davedelong/7371853 to your computer and use it in GitHub Desktop.
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)
@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