If you have ever been faced with the design of a complicated object,
you know how helpful delegation can be. Most of the Apple-provided
user interface elements use or require delegate handlers. Using
delegation instead of subclassing allows for a single central
controller object. For example, in an NSDocument
application, there is usually a document-controller. Using delegation
and data sources for the UI elements, it is easy to use the one
central controller to handle the UI elements. If such features
required subclasses, there would be many decoupled objects for every
UI element that exists.
Using delegation may look easy, but when implementing delegate
management, it is easy to overlook many things. This article presents
some tips on getting it to work properly. As a bonus, it includes an
NSProxy
subclass which forwards messages to an object
if-and-only-if the proxied class responds to the selector.
Developing a delegate interface is the most difficult part of the
delegation implementation. The third-party programmer relies on the
ability to catch the most common events
occuring to the object
supporting the delegation. If you are not careful here, the delegate
won't be useful enough and subclassing will be necessary every time,
so cover all the bases. Some tips:
-kitchenSink:
method.self
as the
first argument so that a single object can handle multiple instances
of your object through delegation.NSObject
to alleviate
compiler warnings. The informal protocol should include all possible
delegation methods.NSTableView
has delegate methods that look like tableView:
... This
prevents method name conflicts and is the best place to pass
self
.
Delegate handling is often implemented like this:
... if([_delegate respondsToSelector(gonk:somethingHappened:)]) [_delegate gonk:self somethingHappened:YES]; ...
Clearly you can see why this is error-prone. It is too easy to have
a typo in the selector name and the selector has to be typed
twice. Also, if _delegate is nil
, the message is silently
ignored.
Well, that is a lot to keep track of. Wouldn't it be handy if we could handle all of this using a simple object which the delegation object interacts with?
MDelegateManager
is a simple NSProxy
subclass which accepts any message passed to it. If the proxied object
it represents responds to the passed message, it is forwarded to that
object. Otherwise, it does nothing. Remember that nil
accepts any message and does nothing. This is very similar except that
MDelegateManager
represents another object, which is what
is meant by proxy
.
Following is the interface for the class:
@interface MDelegateManager : NSProxy { id _proxiedObject; BOOL _justResponded,_logOnNoResponse; } -(void)forwardInvocation:(NSInvocation*)invocation; -(id)proxiedObject; -(void)setProxiedObject:(id)proxied; -(BOOL)justResponded; -(void)setLogOnNoResponse:(BOOL)log; -(BOOL)logOnNoResponse; @end
Note that this class inherits from NSProxy
which is
not a subclass of NSObject
. This means that
NSProxy
is a separate root class of
Objective-C. NSProxy
is designed for situations where an
object is needed to represent a remote object (such as with
Distributed Objects) or an expensive object.
Our represented object is neither remote nor expensive, so it is
used here because it implements the minimum number of methods to be a
root class, reducing method conflicts between the represented object
and the NSProxy
subclass. For example,
NSProxy
does not implement -description
, so
there is no ambiguity- -description
will be sent to the
represented object.
In any case, we have methods to set and return the proxied object (the actual delegate object) and some more methods which will be explained later.
You may have heard of message forwarding for Objective-C. It works,
but because messages are not first-class objects in Objective-C, it is
not easy to grasp. An actual message call to a specific object is
represented by an NSInvocation
object and a message is
represented by an NSMethodSignature
object which
describes a message's name and arguments to the runtime.
When an object is sent a message which it does not implement, it is
given a chance to respond through the
-methodSignatureForSelector:
and
-forwardInvocation:
methods. -methodSignatureForSelector:
must return the
NSMethodSignature
associated with the selector. In the
delegation case, the signature is retrieved from the delegate.
-forwardInvocation:
is then given the opportunity to send
the message to the correct object.
Let's take a look at how they are used:
@interface NSMethodSignature (objctypes) +(NSMethodSignature*)signatureWithObjCTypes:(const char*)types; @end @implementation MDelegateManager -(id)init { _proxiedObject=nil; _justResponded=NO; _logOnNoResponse=NO; return self; }
Unforunately, in order to get this object to work properly, the use
of an undocumented
method which creates an
NSMethodSignature
from a C string is required. You will
see shortly why this is necessary. The category on
NSMethodSignature
is necessary to eliminate the
unknown message
compiler warning. The -init
method
above effectively does nothing. Note that NSProxy
does
not implement -init
, so we do not call [super
init]
.
-(NSMethodSignature*)methodSignatureForSelector:(SEL)selector { NSMethodSignature *sig; sig=[[_proxiedObject class] instanceMethodSignatureForSelector:selector]; if(sig==nil) { sig=[NSMethodSignature signatureWithObjCTypes:"@^v^c"]; } _justResponded=NO; return sig; }
-methodSignatureForSelector
is called when a compiled
definition for the selector cannot be found. This is where we use the
secret, undocumented
method to create our own dud
NSMethodSignature
. This is necessary because if this
method returns nil
, a selector not found
exception
is raised. The string argument to
-signatureWithObjCTypes:
outlines the return type and
arguments to the message. To return a dud
NSMethodSignature
, pretty much any signature will
suffice. Since the -forwardInvocation
call will do
nothing if the delegate does not respond to the selector, the dud
NSMethodSignature
simply gets us around the exception.
-(void)forwardInvocation:(NSInvocation*)invocation { if(_proxiedObject==nil) { if(_logOnNoResponse) NSLog(@"Warning: proxiedObject is nil! This is a debugging message!"); return; } if([_proxiedObject respondsToSelector:[invocation selector]]) { [invocation invokeWithTarget:_proxiedObject]; _justResponded=YES; } else if(_logOnNoResponse) { NSLog(@"Object \"%@\" failed to respond to delegate message \"%@\"! This is a debugging message.",[[self proxiedObject] class],NSStringFromSelector([invocation selector])); } return; }
Following the code, we have a special case for
_proxiedObject
being nil
. Specifically, if
our debug variable _logOnNoResponse
is on, we print some
potentially helpful information about the condition. This relieves the
"why-isn't-the-method-getting-called" syndrome when the programmer
(for example, AgentM) forgets to set the delegate. However, it is not
invalid to have a nil
delegate, so we just
return
.
Next, if the _proxiedObject
does implement the
requested selector, we invoke the invocation and set the special
variable _justResponded
. This variable can be accessed by
the object using this class to check if the delegate actually
responded and behave accordingly- this is useful in some limited
circumstances. Continuing on, if the proxied object does not respond
to the selector and the logging variable is true, then we print an
informative debugging message.
The rest of the code is standard boiler-plate accessors:
-(id)proxiedObject { return _proxiedObject; } -(void)setProxiedObject:(id)proxied { _proxiedObject=proxied; //do not retain- could create circular references } -(BOOL)justResponded { return _justResponded; } -(void)setLogOnNoResponse:(BOOL)log { _logOnNoResponse=log; } -(BOOL)logOnNoResponse { return _logOnNoResponse; } @end
One thing to note here is that the delegate is not
-retain
ed. This is because it could cause circular cases
of retention.
So let us use the new class which makes delegation a breeze! The
following code demonstrates how an object might use
MDelegateManager
.
//****Gonk.h @interface Gonk : NSObject { MDelegateManager *_delegate; } ... @end //****Gonk.m @interface MDelegateManager (GonkDelegate) -(void)gonk:(Gonk*)gonk somethingHappened:(BOOL)continue @end @implementation Gonk -(id)init { ... _delegate=[[MDelegateManager alloc] init]; [_delegate setLogOnNoResponse:YES]; ... } -(void)setDelegate:(id)delegate { [_delegate setProxiedObject:delegate]; } -(id)delegate { return [_delegate proxiedObject]; } -(void)somethingHappened { ... [_delegate gonk:self somethingHappened:YES]; //Wow! That's it! ... } @end
From the code, you can plainly see that calling a delegate is now
as simple as sending any other message. Take a look at the
GonkDelegate
category on
MDelegateManager
. This squelches compiler warnings
concerning undeclared messages being sent later in the code.
There are still unanswered questions here. How is deallocation
handled? NSProxy
does not respond to -retain
or -release
, but it doesn't matter- these messages are
forwarded through to the delegate (probably not very useful). However,
because MDelegateManager
does not use the retain count
method, it must be explicitly -dealloc
ated. So the
following method should be added to class Gonk
.
-(void)dealloc { [_delegate dealloc]; //special case! [super dealloc]; }
Note that we must call -dealloc
here because NSProxy
does respond to it and the message will not be forwarded. Alternatively, we could implement retention count on the subclass, but that would be more work for not much benefit.
The second question is: what if I want the delegate message to return a value? That is what the _justResponded
variable is for; by checking if the delegate actually responded, then the return value is valid. Some demonstration code:
//delegate implements -(BOOL)gonk:(Gonk*)gonk somethingHappened:(BOOL)itDid; //Gonk.m -(BOOL)somethingHappened { BOOL ret; ret=[_delegate gonk:self somethingHappened:YES]; if([_delegate justResponded]) return ret; return NO; }
If the delegate did respond, then it is safe to use what it
returned, if not, don't use it. The Objective-C genius will note that,
for a BOOL
, using the value from a failed
-forwardInvocation:
is safe, however, the method above
extends to other types such as structures.
Using some NSProxy
magic, it's not hard to make
delegation really elegant and very simple. It would be very simple to
add extensions to this class. One useful extension could be posting a
notification based on the forwarded selector so that a simple
[_delegate gonk:self message:YES]
messages the delegate
and constructs and posts a notification.
Stay tuned to this website for more useful NSProxy
subclasses!
This article is in the public domain.