Usage of delegates is prevalent throughout the SDK’s that Apple provides. A delegate is basically a way to tell a standard object “Hey you, if you need some info or want to inform somebody when something happens, talk to this guy. I’m outta here”.
This achieves three things that are an essential skills in programming:
- you make use of a component without having to do extra work of subclassing it for customization
- you appoint somebody else to do the work of one-the-fly customizing (the delegate)
- you can excuse yourself and go to the pool
A good example of usage of delegates is UITableView. This standard class, to be found in UIKit, knows and speaks two protocols: UITableViewDataSource and UITableViewDelegate. As the names suggest the first deals with questions related to the content of the table to be displayed: number of sections, number of rows, headers, individual row cells et cetera. The second delegate, which is also called delegate, deals with interactions on a higher level, like if you tapped on an individual cell. Even though they are called different, both are delegation protocols and if you like you can assign discrete classes to data source and delegate for a table view.
To make this a practical example I will show how to make a class that informs your code when a headset is plugged in or plugged out.
For delegation to work you need four things:
- Define a protocol
- Implement protocol methods inside your delegate class
- Receive and store a pointer to the delegate inside in class that is supposed to call the delegate
- Whenever a delegate call should occur make sure that the delegate actually has implemented the method you want to call
So first, define the protocol methods we like. You can make some mandatory and some others optional or even all optional.
I’ll call the class AudioNotifications.h and the top looks like this. The standard for the naming of protocols is to call it the same as the class and suffix …Delegate.
@protocol AudioNotificationsDelegate -(void)headsetPlugged:(BOOL)pluggedIn; @end |
We want our default view controller to be informed of the changes, so we #import the header there as well and implement this method. The might be something more useful to do than NSLog-ing, for example display an icon or so some other action in response. In the end this method will be the call-back whenever this headset plug event occurs.
#pragma mark AudioNotifications Delegate Method -(void)headsetPlugged:(BOOL)pluggedIn { if (pluggedIn) { NSLog(@"Something was plugged in"); [pluggedLabel setAlpha:1]; } else { NSLog(@"Now we are using the speaker"); [pluggedLabel setAlpha:0]; } } |
Adding protocol methods to a class is called “implementing the protocol”. To mark the whole class as implementor of certain protocols you also have to add the protocol name in angle brackes to the definition like so:
#import #import "AudioNotifications.h" @interface AudioTestViewController : UIViewController { IBOutlet UILabel *pluggedLabel; AudioNotifications *myNot; } @property (nonatomic, retain) IBOutlet UILabel *pluggedLabel; @end |
Note that for this example I added a label to the viewcontrollers default xib and hooked it up to an outlet so that I can show and hide it by means of setting the alpha property.
Next we need to remember our delate somehow. This could be done by simple creating a property or by passing it in a custom init like so in AudiNotifications.m. It seems to be standard to not retain the delegate but only assign it. Don’t ask me why.
- (id) initWithDelegate:(id)aDelegate { delegate = aDelegate; // need to initialize a session so that everything below works AudioSessionInitialize(NULL, NULL, NULL, NULL); // register the property change listener; AudioSessionPropertyID routeChangeID = kAudioSessionProperty_AudioRouteChange; AudioSessionAddPropertyListener (routeChangeID,audioRouteChangeListenerCallback,self); return self; } |
The above code establishes a NULL audio session and registers a callback to a change of one property. This callback is a C-function that I implemented like this:
// A useful method to get the current audio route ... - (NSString *)currentAudioRoute { CFStringRef route; UInt32 propertySize = sizeof(CFStringRef); if (AudioSessionGetProperty(kAudioSessionProperty_AudioRoute, &propertySize, &route)==0) { return (NSString *)route; // this is called "toll-free bridging" } else { // most likely we are in simulator return @"Speaker"; } } // ... because the call back is only informed that a device has changed void audioRouteChangeListenerCallback (void *inUserData, AudioSessionPropertyID inPropertyID, UInt32 inPropertyValueSize, const void *inPropertyValue) { if (inPropertyID != kAudioSessionProperty_AudioRouteChange) return; AudioNotifications *self = (AudioNotifications *)inUserData; if ([[self currentAudioRoute] isEqualToString:@"Speaker"]) { if (self.delegate&&[(NSObject *)self.delegate respondsToSelector:@selector(headsetPlugged:)]) { [self.delegate headsetPlugged:NO]; } } else { if (self.delegate&&[(NSObject *)self.delegate respondsToSelector:@selector(headsetPlugged:)]) { [self.delegate headsetPlugged:YES]; } } } |
This is the most difficult part to understand but it also holds the most programmatic beauty. Every time I want to call the delegate method I first check if the self.delegate is set at all. Next I check if it knows how to respond to the headsetPlugged: message. Here I use a trick. I don’t care about the class of the delegate but only that it “speaks” AudioNotificationsDelegate protocol. So the IVAR is defined as id. But in order to be able to use the respondsToSelector method I found I needed to cast it to an NSObject. You could probably also add appropriate protocol to the delegate pointer definition, but it works like this and that’s what I am proud of.
The final part now is to initialize this class. For lack of a better spot I put it into the viewDidLoad method, not forgetting the appropriate release in dealloc.
myNot = [[AudioNotifications alloc] initWithDelegate:self]; |
That’s it. You can now have any class you like be notified of the headset change or any other thing you want to be able to delegate. Protocols are also great for doing asynchronous stuff on an NSOperation. And if you have a custom view controller creating some new object the most elegant method I have seen is to respond to the class opening the view controller with something like didFinishAddingSpecialObject:
Granted it is slightly more work to structure your code to use protocol-based callbacks like this. But the result is that you get orders of magnitude easier-to-read code. That’s why I have begun to promote the use of protocols.
Download the demo project here: AudioTest.zip
Categories: Recipes
Nice article. I have two additions:
“It seems to be standard to not retain the delegate but only assign it. Don’t ask me why.”
Because in most cases, the delegate owns (and therefore retains) the delegating object. If both objects retained each other, the delegate would not be deallocated when it is released by its own parent (because its retain count is still > 0). In the end, both delegate and delegating object will never be deallocated.
“But in order to be able to use the respondsToSelector method I found I needed to cast it to an NSObject.”
You can avoid the NSObject type cast by requiring your protocol to conform to the NSObject protocol:
@protocol AudioNotificationsDelegate
…
@end
NSObject is not only a class but there is also an NSObject protocol that declares the same methods as the class. Thus, every NSObject subclass conforms to the NSObject protocol. In practice, including in your protocol definition means that your delegate must be an instance of an NSObject subclass.
Sorry, that should read:
@protocol AudioNotificationsDelegate <NSObject>
Your comment system ate the angle brackets.
Hi, useful article – thanks.
Quick question, in the code snippit:
@interface AudioTestViewController : UIViewController {
IBOutlet UILabel *pluggedLabel;
AudioNotifications *myNot;
}
@property (nonatomic, retain) IBOutlet UILabel *pluggedLabel;
@end
…do I really need the “IBOutlet” inside the interface declaration? Won’t it be ok, *only* to have it in the property declaration later?
You are correct if the label is generated via code. But in this case it is instantiated from a XIB, thus an outlet is necessary.
Ah, cheers, understood. I still have a lot to learn 😀
gud tutorial
gud article….