lundi 12 août 2013

Forwarding mechanism

In this article you'll learn about the Cocoa's Forwarding Mechanism.
I'll explain it starting by how i wrote the STCollapseTableView component, a UITableView subclass, that automatically collapse and/or expand sections, that you use exactly like a classic table view.


STCollapseTableView : The issue


First thing I wanted to do when I designed this component is that we can use it exactly like a classic table view.

For that I override the setDatasource: method of UITableView and store the object in parameters, the original datasource, in a new property, collapseDatasource, while I call the original implementation, on super, passing self as argument. I do the exact same thing for the delegate.

That way I will be able to implement the tableView:numberOfRowsInSection: method and return 0 for a closed section whereas the original datasource returns several rows.

This works fine but then a problem will occur: the UITableViewDatasource protocol declares a lot of methods, and I'm not yet talking about the delegate. So if I want all the UITableView features available on my component, I need to implement all the protocol's methods and call the original datasource for each. That is not acceptable because some UITableView behaviors will not be the same if we implement, or not, some methods. For instance the delete button will appear on a cell after an horizontal swipe if we implement the tableView:commitEditingStyle:forRowAtIndexPath:. So how to do if we want our new datasource to implement only the already implemented by the original datasource methods ?

Here comes the Cocoa's forwarding mechanism…

Forwarding Mechanism


The forwarding mechanism is a mighty behavior: If we send a message to an object that don't responds to it (it does not implement the method), generally an "unrecognized selector" exception will raise, but there's a way to prevent that, each object can forward an unknown message to an other object that could respond to it ! To achieve that, any NSObject subclass can override several methods.

The first one that will be called if the object can't responds to a message is:
- (id)forwardingTargetForSelector:(SEL)aSelector;
In that method you can simply returns an other object that could respond to the selector in parameters. If you don't implement this method, or in case you return nil, a mightier but more expensive machinery will take over, an NSInvocation will be create.

NSInvocation is an object that represents the sent message, it contains the target, the selector, the arguments and the returned value.

Before it can be create, the signature of the method needs to be known. For that a method will be called on your object:
- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
A NSMethodSignature is an object that contains the type of the returned value and of any argument of the method. In this implementation you could simply call this method on an other object, or you can create an NSMethodSignature by yourself using the signatureWithObjCTypes: constructor. That method takes a character string as parameter. First character represents the return type, second represents self, third represents _cmd, and then there is one character for each parameter. Each type is represented by a character: 'v' for void, 'i' for an int, '@' for an object, ':' for a selector, … For example, the method signature for the method - (void)addAmount:(int)amount is "v@:i". For more informations and all the available characters, see  the Apple documentation

Once your method signature returned, the invocation will be create and a new method will be called:
- (void)forwardInvocation:(NSInvocation*)invocation;
From that method you can do whatever you want, modify the invocation's selector, fetch an argument, invoke it on an other object, or do nothing so that any unknown message shouldn't raise an exception even if they aren't forwarded.

If you don't implement that method, or if you call the super implementation, a last method will be called:
- (void)doesNotRecognizeSelector:(SEL)aSelector;
If you override this method, you must call the super implementation or raise yourself an NSInvalidArgumentException.

Last thing I didn't wrote: as you certainly know, you can check if an object responds to a selector. By default it checks if the object implement the method and not if it can be forward.
You can of course modify that by overriding the well known method:
- (BOOL)respondsToSelector:(SEL)aSelector;
In your implementation you could call that method on the object a message could be forwarded to. Default implementation of this method returns YES for a method that is really implemented by the object.

STCollapseTableView : The solution


With all those methods we will be able to build our component. As you may understood, our STCollapseTableView will not implement all the UITableViewDatasource protocol but will forward the messages to the original datasource.

First thing, we want our object to respond to the messages our original datasource respond to. Oh and same thing for the delegate.

- (BOOL)respondsToSelector:(SEL)aSelector
{
    return [super respondsToSelector:aSelector] ||
      [self.collapseDataSource respondsToSelector:aSelector] ||
      [self.collapseDelegate respondsToSelector:aSelector];
}

Great! now that our object can respond to a message, it have to really respond to it.

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if ([self.collapseDataSource respondsToSelector:aSelector])
    {
        return self.collapseDataSource;
    }
    if ([self.collapseDelegate respondsToSelector:aSelector])
    {
        return self.collapseDelegate;
    }
    return nil;
}

That works fine !

Last thing, STCollapseTableView automatically toggle a section when the user tap on its header. For that it add a tap gesture recognizer on the header view by implementing the tableView:viewForHeaderInSection: method. Although this method is optional in the protocol so I don't want it to be called if the original datasource don't implement it. To do that I will modify our respondsToSelector: implementation by first checking if the selector is our tableView:viewForHeaderInSection: method, if YES I will only call the respondsToSelector: implementation on our original delegate and not on super. To check the selector I will use a function from objc/runtime.h so don't forget to import it.

- (BOOL)respondsToSelector:(SEL)aSelector
{
    if (sel_isEqual(aSelector,
      @selector(tableView:viewForHeaderInSection:)))
    {
        return [self.collapseDelegate respondsToSelector:aSelector];
    }
    return [super respondsToSelector:aSelector] ||
      [self.collapseDataSource respondsToSelector:aSelector] ||
      [self.collapseDelegate respondsToSelector:aSelector];
}


That's all folks !
You can find this component on my github.

Aucun commentaire:

Enregistrer un commentaire