How to send not declared selector without performSelector:?

276 Views Asked by At

Background: I have an object (let's call it BackendClient) that represents connection with server. Its methods are generated to single @protocol and they are all synchronous, so I want to create proxy object that will call them in background. The main problem is return value, which I obviously can't return from async method, so I need to pass a callback. The "easy" way will be copy all BackendClient's methods and add callback argument. But that's not very dynamic way of solving that problem, while ObjectiveC nature is dynamic. That's where performSelector: appears. It solves problem entirely, but it almost kills proxy object transparency.

Problem: I want to be able to send not declared selector to proxy (subclass of NSProxy) object as if it was already declared. For example, I have method:

-(AuthResponse)authByRequest:(AuthRequest*)request

in BackendClient protocol. And I want proxy call look like this:

[proxyClient authByRequest:myRequest withCallback:myCallback];

But this wouldn't compile because

No visible @interface for 'BackendClientProxy' declares the selector 'authByRequest:withCallBack:'

OK. Let's calm down compiler a bit:

[(id)proxyClient authByRequest:myRequest withCallback:myCallback];

Awww. Another error:

No known instance method for selector 'authByRequest:withCallBack:'

The only thing that comes to my mind and this point is somehow construct new @protocol with needed methods at runtime, but I have no idea how to do that.

Conclusion: I need to suppress this compilation error. Any idea how to do that?

2

There are 2 best solutions below

1
On

To look up a selector at runtime, you can use NSSelectorFromString(), but in this case you should just go ahead and import whatever header you need to get the declaration of -authByRequest:

2
On

If I understand it, you have a synchronous, non-threaded, API that you want to be asynchronous for purposes of not blocking, say, the main event loop, etc...

I would add a serial queue to BackgroundClient:

@property(strong) dispatch_queue_t serialQueue;

 ... somewhere in your -init ...
 _serialQueue = dispatch_queue_create(..., serial constant);

Then:

 - (void)dispatchOperation:(dispatch_block_t)anOperation
 {
      dispatch_async(_serialQueue, anOperation);
 }

That can be used like:

 [myClient dispatchOperation:^{
       [myClient doSynchronousA];
       id result = [myClient doSynchronousB];
       dispatch_async(dispatch_get_main_queue(), ^{
            [someone updateUIWithResult:result];
       }
 }];

That is the easiest way to move the BackgroundClient to an asynchronous model without rewriting it or heavily refactoring it.

If you want to harden the API, then create a class wrapper for BackendClient that holds an instance of the client and the serial queue. Make it such that said class instantiates the client and the rest of your code only retrieves instances from that wrapper. That'll allow you to still have the same dispatchOperation: model, but not require mirroring all the methods.


typedef void(^ AsyncBackendBlock(BackendClient* bc); @interface AsyncBackend +(instancetype)asyncBackendWithBackend:(BackendClient*)bc;

 @property .... serialQueue;

 - (void) dispatchAsync:(AsyncBackendBlock) backBlock;
 @end

.m:

 @interface AsyncBackend()
 @property... BackendClient *client;
 @end

 @implementation AsyncBackend

 - (void) dispatchAsync:(AsyncBackendBlock) backBlock
 {
     dispatch_async(_serialQueue, ^{
         backBlock(_client);
     });
 }
 @end

Caller:

 AsyncBackend *b = [AsyncBackend asyncBackendWithBackend:[BackendClient new]];
 [b dispatchAsync:^(BackendClient *bc) {
    [bc doSomething];
    id result = [bc retrieveSomething];
    dispatch_async(dispatch_get_main_queue(), ^{
         [uiThingy updateWithResult:result];
    }
 }];
 ....