objective c message forwarding with forwardingTargetForSelector not always working

3.4k Views Asked by At

I have a view controller which defines a protocol which itself inherits another protocol. I want any object that implements my protocol to also implement the inherited protocol.

I want to set my class to intercept some of the messages in the inherited protocol in order to configure some things internally but eventually would like to forward all of the messages to the delegate of my class

I could write a lot of boiler plate code to stub all of the protocol and intern call the delegate but I see that it breaks a lot of the time - any time the "super" protocol changes I need to restub this class once again.

I see that this is very predominant in custom UI controls. When reusing existing components - for instance tables or collection views you would like your data source to respond to all of the common protocols but some instances you need to configure the view according to the index or save a particular state.

I've tried using forwardingTargetForSelector in order to forward the messages I do not respond to , but it isn't always forwarding...

Here is a contrived code example:

Class A: (the top most protocol)

@

    protocol classAProtocol <NSObject>

    -(void)method1;
    -(void)method2;
    -(void)method3;

    @end

    My Class

    @protocol MyClassProtocol <classAProtocol>

    -(void)method4;
    @end

    @interface MyClass

    @property (nonatomic,weak> id <MyClassProtocol> delegate;

    @end

    @interface MyClass (privateInterface)

    @property (nonatomic,strong) ClassA *classAObject;
    @end
    @implementation MyClass

    -(init)
    {
      self = [super init];
      if (self)
      {
        _classAObject = [[ClassA alloc] init];
        _classAObject.delegate = self; // want to answer some of the delegate methods but not all
      }
    }

-(void)method1
{
  // do some internal configuration
  // call my delegate with
  [self.delegate method1];
}
    -(id)forwardingTargetForSelector:(SEL)aSelector
    {
        if ([self respondsToSelector:aSelector])
        {
            return self;
        }
        if ([self.delegate respondsToSelector:aSelector])
        {
            return self.delegate;
        }
        return nil;
    }

    -(void)setDelegate:(id <MyClassProtocol>)delegate
    {
      self.delegate = delegate; // will forward some of the messages
    }
    @end
2

There are 2 best solutions below

2
On

You shouldn't ever return self; from forwardingTargetForSelector:. Your check should mean it never is but if you ever did return self it would cause an infinite loop.

You need to be sure that a super class isn't implementing the method as this will prevent forwardingTargetForSelector: from being called. Check if the method is actually called.

forwardingTargetForSelector: is also only called when a method is called on your controller that it doesn't respond to. In your example you aren't calling [self ...];, you're calling [self.delegate ...]; so forwardingTargetForSelector: will not be called.

2
On

Returning self from forwardingTargetForSelector: makes no sense because it would never be called if self responded to the selector.

You need to implement these three methods:

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    if ([self.delegate respondsToSelector:[anInvocation selector]])
        [anInvocation invokeWithTarget:self.delegate];
    else
        [super forwardInvocation:anInvocation];
}

- (BOOL)respondsToSelector:(SEL)aSelector
{
    return [super respondsToSelector:aSelector] || [self.delegate respondsToSelector:aSelector];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
    NSMethodSignature *signature = [super methodSignatureForSelector:selector];
    if (!signature) {
        signature = [self.delegate methodSignatureForSelector:selector];
    }
    return signature;
}