Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Best way check if delegate responds to optional selector

I've got used wrapping sending @optional delegate's methods into:

if ([self.delegate respondsToSelector:@selector(method:)]) {
    [self.delegate method:obj];
}

It works well, but if there are lot of delegate's methods, it looks like duplicating respondToSelector: code...

At some point of time I've put respondsToSelector: to separate method:

//ignore warning
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"

- (void)performDelegateSelector:(SEL)selector withObject:(id)obj {

    if ([self.delegate respondsToSelector:selector]) {
        [self.delegate performSelector:selector withObject:obj];
    }
}

As a result I have just one respondToSelector: check, but It still doesn't look like well improved.

[self performDelegateSelector:@selector(method:) withObject:self];

What do you think? Does it make sense in using some helpers or category to wrap sending all @optional delegate's methods, or it's something that shouldn't be improved?

like image 605
Injectios Avatar asked Sep 23 '13 12:09

Injectios


2 Answers

Performance-wise it depends on what use you make of the delegate, whether it's directly affecting the UI (in which case you want to have faster response times) etc.

A good speed optimization is to register all of the methods implemented by the delegate in a data structure when you first set the delegate. You could do:

struct {
    unsigned int myFirstMethod:1;
    unsigned int mySecondMethod:1;
} delegateRespondsTo;

-(void)setDelegate:(id<MyProtocol>)delegate
{
    self.delegate = delegate;
    delegateRespondsTo.myFirstMethod = [delegate respondsToSelector:@selector(myFirstMethod)];
    delegateRespondsTo.mySecondMethod = [delegate respondsToSelector:@selector(mySecondMethod)];
}

Then you can simply do

if ( delegateRespondsTo.myFirstMethod )
    [self.delegate myFirstMethod];

Check this great answer for a more exhaustive explanation.

like image 150
micantox Avatar answered Oct 28 '22 08:10

micantox


You can use a trampoline for this. A trampoline is an object that forwards messages to another object. Here's a simple DelegateTrampoline.

#import <objc/runtime.h>

@interface DelegateTrampoline : NSObject
- (id)initWithProtocol:(Protocol *)protocol delegate:(id)delegate;
@end

#import "DelegateTrampoline.h"

@interface DelegateTrampoline ()
@property (nonatomic) Protocol *protocol;
@property (nonatomic, weak) id delegate;
@end

@implementation DelegateTrampoline

- (id)initWithProtocol:(Protocol *)protocol delegate:(id)delegate {
  self = [super init];
  if (self) {
    _protocol = protocol;
    _delegate = delegate;
  }
  return self;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
  // Look for a required method
  struct objc_method_description desc = protocol_getMethodDescription(self.protocol, selector, YES, YES);
  if (desc.name == NULL) {
    // Maybe it's optional
    desc = protocol_getMethodDescription(self.protocol, selector, NO, YES);
  }
  if (desc.name == NULL) {
    [self doesNotRecognizeSelector:selector]; // Raises NSInvalidArgumentException
    return nil;
  }
  else {
    return [NSMethodSignature signatureWithObjCTypes:desc.types];
  }
}

- (void)forwardInvocation:(NSInvocation *)invocation {
  SEL selector = [invocation selector];
  if ([self.delegate respondsToSelector:selector]) {
    [invocation setTarget:self.delegate];
    [invocation invoke];
  }
}

@end

Here's how you would use it:

@interface MyObject ()
@property (nonatomic) id delegateTrampoline;
@end
...
self.delegateTrampoline = [[DelegateTrampoline alloc] initWithProtocol:@protocol(MyProtocol)
                                                              delegate:delegate];

[self.delegateTrampoline myobject:self didSomethingAtIndex:1];
@end

Some notes:

  • This is very slow compared to other approaches. It requires creating an NSInvocation object, which can be hundreds of times slower than a simple method call. That said, unless you're calling your delegate method in a tight loop, it's probably not a problem.
  • You must declare delegateTrampoline to be of type id. This allows you to pass arbitrary selectors to it. But it also means that the compiler cannot detect if you pass a selector to delegateTrampoline that is not in the protocol. You'll crash at runtime if you do this. The compiler can detect if you pass a totally unknown selector, so it'll catch simple typos.
like image 42
Rob Napier Avatar answered Oct 28 '22 10:10

Rob Napier