Injectios
Injectios

Reputation: 2797

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?

Upvotes: 8

Views: 8790

Answers (3)

Bartosz Szczepaniak
Bartosz Szczepaniak

Reputation: 25

On top of answer provided by @micantox I would add that instead of struct you could use NS_OPTIONS to define a bitmask.

typedef NS_OPTIONS (NSUInteger, MyDelegateOptions) 
{ 
  MyDelegateOptionDidFinishedWithTaskOne = 1 << 0,
  MyDelegateOptionDidFinishedWithTaskTwo = 1 << 1,
  MyDelegateOptionDidFinishedWithTaskThree = 1 << 2
};

You could then create property that can hold bitmask:

@property (nonatomic, assign) MyDelegateOptions delegateOptions;

and do check if delegate respondsToSelector:in your setter like

- (void)setDelegate:(id<MyDelegateProtocol>)delegate
{
  _delegate = delegate;
  if ([delegate respondsToSelector:@selector(myDelegateDidFinishedWithTaskOne:)]) {
  {
    self.delegateOptions = self.delegateOptions | MyDelegateOptionDidFinishedWithTaskOne;
  }

  if ([delegate respondsToSelector:@selector(myDelegateDidFinishedWithTaskTwo:)]) {
  {
    self.delegateOptions = self.delegateOptions | MyDelegateOptionDidFinishedWithTaskTwo;
  }

  if ([delegate respondsToSelector:@selector(myDelegateDidFinishedWithTaskThree:)]) {
  {
    self.delegateOptions = self.delegateOptions | MyDelegateOptionDidFinishedWithTaskThree;
  }
}

Then you can simply check assigned bit patterns by using bitwise AND

if (self.delegateOptions & MyDelegateOptionDidFinishedWithTaskOne) {
    [self.delegate myDelegateDidFinishedWithTaskTwo:myResult];
}

As you can see use of bitwise AND will return true if bit for MyDelegateOptionDidFinishedWithTaskOne is set as 1 even when other bits are set as well.

So if you assume that delegate was checked in your setter and based on this your delegateOptions holds bit pattern of 00000111 and your MyDelegateOptionDidFinishedWithTaskOne represents bit pattern of 00000001 then by simply using bitwise AND

00000111 & 00000001 = 00000001

you'll get true with your conditional statement.

It is worth notting that this is overkill in situations when you need to check your delegate only once or twice.

Upvotes: 1

micantox
micantox

Reputation: 5616

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.

Upvotes: 13

Rob Napier
Rob Napier

Reputation: 299355

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.

Upvotes: 4

Related Questions