UITabBarController
is generally used as is, no subclassing required. It creates a UITabBar
and manages a list of UIViewControllers
, keeping track of the tab in focus, UI creation and everything nice. UITabBarController
has a delegate, the UITabBarControllerDelegate
protocol. Unfortunately this is not a superset of the UITabBarDelegate
protocol, and UITabBarController
already implements the UITabBarDelegate
protocol itself. So how can I hook into and respond to delegate calls from the managed UITabBar
?
Simply replacing the original delegate will break the default functionality. We need to somehow insert our own code to execute before the original delegates call. Or in AOP speak; a before advice.
The Wanted Solution
What we want is a proxy that can forward invocations both to our own delegate, and to the original delegate. The interface for said class looks looks like this:
@protocol CWDelegateOverrideProxyDelegate
@required
-(BOOL)shouldCallOriginalImplementationForSelector:(SEL)aSelector;
@end
@interface CWDelegateOverrideProxy : NSProxy {
@private
id _originalDelegate;
id _overrideingDelegate;
id _delegate;
}
@property(nonatomic, retain, readonly) id originalDelegate;
@property(nonatomic, retain, readonly) id overridingDelegate;
@property(nonatomic, assign) id delegate;
-(id)initWithOriginalDelegate:(id)originalDelegate
overridingDelegate:(id)overridingDelegate;
@end
And then we could use this class to replace an existing delegate with a short piece fo code like this:
UITabBar* tabBar = (UITabBar*)[self.tabBarController.view
viewWithKindOfClass:[UITabBar class]];
CWDelegateOverrideProxy* proxy = [[CWDelegateOverrideProxy alloc]
initWithOriginalDelegate:tabBar.delegate overridingDelegate:self];
tabBar.delegate = (id)proxy;
See my previous post for the implementation of viewWithKindOfClass:
.
Easy enough to use, only thing left is to implement CWDelegateOverrideProxy
.
Enter NSProxy
Java only have one root class; java.lang.Object
. Objective-C can have many, and Cocoa defines two; NSObject
and NSProxy
. Both of them conforms to the protocol NSObject
, wich can be confusing. What we need to know is that class names, and protocol names have two different name spaces in Objective-C. Imagine it as if you could have a class and an interface with the same name in Java; in Objective-C you can. NSObject
class is the root class for all concrete classes, and NSProxy
class is the root class for proxies, both implements NSObject
protocol allowing them to be interchangeable.
Without much ado, here is the base implementation for the initializer:
#import "CWDelegateOverrideProxy.h"
@implementation CWDelegateOverrideProxy
@synthesize originalDelegate = _originalDelegate;
@synthesize overridingDelegate = _overridingDelegate;
@synthesize delegate = _delegate;
-(id)initWithOriginalDelegate:(id)originalDelegate
overridingDelegate:(id)overridingDelegate;
{
_originalDelegate = [originalDelegate retain];
_overridingDelegate = [overridingDelegate retain];
return self;
}
// More code goes here!
@end
Introspection
In Objective-C, just as in Java, an instance can be queried for its class, and conformance to protocols (interface in Java speak). But as a bonus an instance can also be queried for specific methods per instance.
Our CWDelegateOverrideProxy
class inherits from NSProxy
. And it makes a simple assumption; if the original delegate, or the overriding delegate is capable of something, then so it is. So if queried for type of class, protocol conformance, or implemented methods, then we reply with whatever our managed delegates replies.
The NSObject
protocol defines methods, equivalent of the java.lang.reflect
package, that allows us to implement this as such:
#pragma mark --- Testing Object Behavior, and Conformance
-(BOOL)isKindOfClass:(Class)aClass;
{
return [self.overridingDelegate isKindOfClass:aClass] ||
[self.originalDelegate isKindOfClass:aClass];
}
-(BOOL)conformsToProtocol:(Protocol *)aProtocol;
{
return [self.overridingDelegate conformsToProtocol:aProtocol] ||
[self.originalDelegate conformsToProtocol:aProtocol];
}
-(BOOL)respondsToSelector:(SEL)aSelector;
{
return [self.overridingDelegate respondsToSelector:aSelector] ||
[self.originalDelegate respondsToSelector:aSelector];
}
Invocation Forwarding
The NSObject
protocol also defines methods for functionality equivalent of java.lang.reflect.InvocationHandler
and hooks to act as a java.lang.reflect.Proxy
. The details are different as Objective-C do not enforce calls to only known methods at compile time, which means that at run-time unimplemented methods can be called.
When an unimplemented method is called the run-time will call methodSignatureForSelector:
, that can return a NSMethodSignature
instance, sort of the equivalent of a java.lang.reflect.Method
. The run-time will then use the information from the NSMethodSignature
instance to create a correct NSInvocation
(other half of a java.lang.reflect.Method
) instance, and set the correct arguments. This NSInvocation instance is then used as argument to call forwardInvocation:
.
And thus our implementation is completed with:
#pragma mark --- Handling Proxy Methods
-(NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector;
{
if ([self.overridingDelegate respondsToSelector:aSelector]) {
return [self.overridingDelegate methodSignatureForSelector:aSelector];
} else if ([self.originalDelegate respondsToSelector:aSelector]) {
return [self.originalDelegate methodSignatureForSelector:aSelector];
} else {
return [super methodSignatureForSelector:aSelector];
}
}
-(void)forwardInvocation:(NSInvocation*)anInvocation;
{
BOOL shouldCallOriginal = YES;
SEL aSelector = [anInvocation selector];
if ([self.overridingDelegate respondsToSelector:aSelector]) {
[anInvocation setTarget:self.overridingDelegate];
[anInvocation invoke];
if (self.delegate != nil) {
shouldCallOriginal = [self.delegate shouldCallOriginalImplementationForSelector:aSelector];
}
}
if (shouldCallOriginal && [self.originalDelegate respondsToSelector:aSelector]) {
[anInvocation setTarget:self.originalDelegate];
[anInvocation invoke];
}
}
And that rounds up how to implement a class that handles proxy based AOP, in order to introduce a before advice for delegate methods. For a more complex code base you can look at HessianKit, a framework that uses proxies, invocation forwarding and dynamic class creation in order to use a web service as distributed objects.
Thanks Fredrik!
Typo alert: the _overridingDelegate instance variable is misspelled as “_overrideingDelegate” when its being defined as a private instance variable in the interface file.
Fantastic article!
I may put a little AOP in my new dependency injection for objective-C project:
https://github.com/jasperblues/spring-objective-c/blob/master/README.md
One nice thing would be something similar to Spring’s @Configurable to swizzle the intializer, and give dependency injection on an object outside of the context of the container.