Friday, May 27, 2016

Objective-C NSInvocation Tutorial

NSInvocation definition and description as per Apple documentation,


"An NSInvocation is an Objective-C message rendered static, that is, it is an action turned into an object.NSInvocation objects are used to store and forward messages between objects and between applications, primarily by NSTimer objects and the distributed objects system.

An NSInvocation object contains all the elements of an Objective-C message: a target, a selector, arguments, and the return value. Each of these elements can be set directly, and the return value is set automatically when the NSInvocation object is dispatched."




                         


Let's discuss the definition and use cases of NSInvocation with an example in Objective-C.

NSInvocation mainly comes into picture,

  1. When we have undo actions in our applications.
  2. When we have queue of methods to be called for latter use or after a specific delay.
  3. When there are more than 2 arguments for performing a selector.

#1 When we have undo actions in our applications.


Let's say I have a shopping cart app. User should have an option to add a product to the cart as well as deleting the product from the cart. If user deletes a product from shopping cart, app should have an option to undo that. I mean adding back the product to the cart.

We generally stores list of items in an Array. We store product ids in the cart in an array. When user deletes a product from the cart, we delete that particular product ID from our cart array. We need to store these deleted product IDs some where as we are having undo action for that.


                         



Let's see this action in Objective-C using NSInvocation.

To add any product to the cart,



  
  - (void)addProduct:(NSString*)productID atIndex:(int)indexPosition{

          [cartArray insertObjectAtIndex: indexPosition];

          //Reload shopping cart
     }

     [self addProduct:@"225" atIndex: cartArray.count-1];


To delete any product to the cart,


  [self deleteProductFromCartAtIndex:2];

  - (void) deleteProductFromCartAtIndex:(int)indexPosition{
         
        //Deleting product from cart

        NSString *deletedProductID = [cartArray objectAtIndex: indexPosition];
        [cartArray removeObjectAtIndex:indexPositon];
  
        // Saving entire action for undo action of product deletion

         NSMethodSignature *methodSig = [self 
                       methodSignatureForSelector:@selector(addProduct:atIndex:)];

         NSInvocation *undoProductDeletion = [NSInvocation 
                                                  invocationWithMethodSignature:methodSig];

         [undoProductDeletion setTarget:self];
         [undoProductDeletion setSelector:@selector(addProduct:atIndex:)];
         [undoProductDeletion setArgument:&deletedProductID atIndex:2];
         [undoProductDeletion setArgument:& indexPosition atIndex:3];
         [undoProductDeletion retainArguments];

         [undoProductDeletionActions addObject: undoProductDeletion];

         //Reload shopping cart


  }




If you look at the delete method, We need to store the values for undo action like product ID, index of product etc..
We can actually store these values in another array and call the addProduct method with these values. The reason for going to NSInvocation for this is, We are storing entire action here in a raw format which we can change dynamically at run time also. And also I can simply invoke the undo action as shown below using the NSInvocation invoke method.



      NSInvocation *undo = [undoProductDeletionActions objectAtIndex:0];

      [undo invoke];


We can also change the target at run time using,


      [undo invokeWithTarget:myClassObj]; 
//instead of self


As simple as that.

Let's discuss the invocation creation line by line.


   NSMethodSignature *methodSig = [self 
                         methodSignatureForSelector:@selector(addProduct:atIndex:)];


   NSInvocation *undoProductDeletion = [NSInvocation 
                                                    invocationWithMethodSignature:methodSig];


We have taken the addProduct method signature to create the NSInvocation object.


      [undoProductDeletion setTarget:self];


Setting the target for the action. Could be any class having this addProduct method.


      [undoProductDeletion setSelector:@selector(addProduct:atIndex:)];


Setting the method to be called upon invocation. Here it is addProduct.


     [undoProductDeletion setArgument:&deletedProductID atIndex:2];


     [undoProductDeletion setArgument:& indexPosition atIndex:3];


Setting the arguments of the selector. Here product iD and index position. We need to pass the pointers (&deletedProductID) instead of actual values. We are adding these parameters starting from index 2. Because the index 0 is occupied by the target and index 1 is occupied by the selector. So we have to start the arguments from index 2.



      [self addProduct:@"223" atIndex:2];





targetself0
selectoraddProduct1
&deletedProductID2232
&indexPosition23



      [undoProductDeletion retainArguments];



Retaining arguments to avoid issues when there is a chance of arguments getting deallocated.

Thus as per the NSInvocation definition, It is an action turned into an object. As these actions became static and objects now, We can use them to forward messages between objects and between applications. We can pass the same invocation we have created to another object if it's having the implementation for the selector 'addProduct'.


      [undo invokeWithTarget:myClassObj]; 


In obj1 class, We can pass NSInvocation as parameter to another object obj2. But, to use that obj2 should have the implementation of the selector 'addProduct'.


  
  [obj2 setInvocationCall: undoProductDeletion];

    -(void) setInvocationCall:(NSInvocation*) undoProductDeletion{

            [undoProductDeletion invokeWithTarget:obj2];
    }



#2 When we have queue of methods to be called for latter use or after a specific delay.


Timers can be scheduled using NSInvocation instead of entire method signature.


  
NSInvocation *inv = [NSInvocation invocationWithMethodSignature:sig];

  [inv setSelector:@selector(methodWithArg1:and2:)];
  [inv setTarget:self];
  [inv setArgument:&arg1 atIndex:2];  
  [inv setArgument:&arg2 atIndex:3];

  theTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
                                        invocation: inv
                                           repeats:YES];




#3 When there are more than 2 arguments for performing a selector.


performSelector works for methods having max 2 parameters. If a method is having more than 2 parameters, It doesn't work. We need to go for NSInvocation for that situation.




     
[self performSelector:myMethod3 withObject:@"val1" withObject:@"val2"];  




Hope this post is useful. Feel free to comment incase of any queries.