8

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?

Michael Ho Chum
  • 939
  • 8
  • 17
Injectios
  • 2,777
  • 1
  • 30
  • 50

3 Answers3

13

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.

Community
  • 1
  • 1
micantox
  • 5,446
  • 2
  • 23
  • 27
4

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.
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • sounds good, but I meant - how to reduce amount of code, but not to reduce the performance.. And if there are lot of delegate's methods are executed, it can affects app perofrmance – Injectios Sep 23 '13 at 20:42
  • If by lots you mean a dozen or two dozen calls, this will have no significant impact on performance. If you lots you mean hundreds or thousands of calls in a tight loop, then yes, then it might have a performance impact. The difference between a nanosecond and a millisecond is massive, but on a dozen calls you'll never notice the difference. – Rob Napier Sep 23 '13 at 21:42
  • 1
    I've found similar approach here http://borkware.com/rants/agentm/elegant-delegation/ using NSProxy – Injectios Mar 06 '15 at 13:05
1

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.

Community
  • 1
  • 1