Get the true responder to an ObjC method when it is being forwarded

86 Views Asked by At

I came across a case where a UITableView/UICollectionView delegate is being forwarded by a proxy object (not an NSProxy, just a regular object).

Depending on the specific delegate method, the proxy will forward it to one of 2 objects which will actually respond to the method.

Given a delegate callback, I would like to know the "true" instance which is responding to the method call.

The proxy code looks like:

@implementation DelegateSplitter

- (instancetype)initWithFirstDelegate:(id<NSObject>)firstDelegate secondDelegate:(id<NSObject>)secondDelegate
{
    if(self = [super init])
    {
        _firstDelegate = firstDelegate;
        _secondDelegate = secondDelegate;
    }

    return self;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    SEL aSelector = [anInvocation selector];

    if([self.firstDelegate respondsToSelector:aSelector])
    {
        [anInvocation invokeWithTarget:self.firstDelegate];
    }

    if([self.secondDelegate respondsToSelector:aSelector])
    {
        [anInvocation invokeWithTarget:self.secondDelegate];
    }
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *first = [(NSObject *)self.firstDelegate methodSignatureForSelector:aSelector];
    NSMethodSignature *second = [(NSObject *)self.secondDelegate methodSignatureForSelector:aSelector];

    if(first)
    {
        return first;
    }
    else if(second)
    {
        return second;
    }

    return nil;
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if([self.firstDelegate respondsToSelector:aSelector] || [self.secondDelegate respondsToSelector:aSelector])
    {
        return YES;
    }
    else
    {
        return NO;
    }
}

@end

My code looks like:

Given a delegate method I want to know which instance is responding:

    // delegate is an instance of DelegateSplitter
    id <UITableViewDelegate> delegate = tv.delegate; 

    SEL didSelectItemSelector = @selector(collectionView:didSelectItemAtIndexPath:);

    if ([delegate respondsToSelector:didSelectItemSelector]) {
       // the delegate splitter doesn't forward the call
       ...
       return;
    }

// the delegate (proxy) forwards the method to a different instance
    if (![delegate.class instancesRespondToSelector:didSelectItemSelector]) {
            // the delegate responds to selector but the class instances themselves do not respond to the selector. This is possible if the delegate is forwarding all invocations to a different object

               NSObject *d = (NSObject *)delegate;
         NSMethodSignature *ms = [d methodSignatureForSelector:didSelectItemSelector];

         if (ms) {

          ** I WANT TO GET THE INSTANCE WHICH IS RESPONDING**
          ????
          HOW DO I GET IT
          ?????
         }
    }

EDIT:

Current hack in place (would like something more holistic):

Make the forwarder invoke on a mock object and take the target object

@implementation NSObject (HACK)
- (id)responderToSelector:(SEL)selector
{
  if ([self respondsToSelector:selector] && [self.class instancesRespondToSelector:selector]) {
    return self; // the class and the instance actually will respond to the selector when called
  }

  if ([self respondsToSelector:selector]) {
    // invocations are forwarded
    id forward = [self forwardingTargetForSelector:selector];
    if (forward && forward != self) {
      return [forward responderToSelector:selector];
    }

      NSMethodSignature *ms = [self methodSignatureForSelector:selector];
      if (ms) {

        MockInvocation *mockInvocation = [MockInvocation invocationWithMethodSignature:ms];
        mockInvocation.selector = selector;
        [self forwardInvocation:mockInvocation];
        return mockInvocation.target ?: mockInvocation.innerTarget;
      }

  }
  return nil;
}


@interface MockInvocation : NSInvocation

@property (nonatomic, weak) id innerTarget;
@end

@implementation IIOMockInvocation

- (void)invoke
{
}
- (void)invokeWithTarget:(id)target
{
  _innerTarget = target;
}

@end
0

There are 0 best solutions below