Protocols part 2 : Delegation

Mark Dalrymple

10 min read

Apr 11, 2012

Protocols part 2 : Delegation

Last time we talked about protocols and why you’d want to use one. So, when would you want to make your own protocol? You make protocols when you’re defining some kind of mechanism for your object to use other objects to do its work. One use of protocols is defining the set of methods used for plugins. AMOSXP(3) has a section that builds plugins in Cocoa, using a protocol to spec out how the plugin and the host application interact with each other.

Another use for making your own protocol is to set up a delegate relationship between two objects. That’s what I’ll show here.

The class providing the delegate

The example for this posting is making a world map view. You can pick up the code from here. The code is extensively commented, and covers details (like how the map was created) that aren’t covered here.

After an extensive design session these features were deemed necessary for the map:

  • Draw the countries of the world

  • Draw each country in an arbitrary color

  • Let the user click on each country and have something happen

I expect the map will be in a window, and there will be a controller object (an App Controller, or a View Controller) that manages it.

There’s a number of different ways you can architect the map view to satisfy the feature list. One way is to have the controlling object supply an array or dictionary of colors to the view, and use target/action to handle clicks.

The way I’m going to do it here is to use a delegate object. The App Controller will be the object used as the delegate. The map will ask its delegate what color each country should be. The map will inform its delegate when a country is clicked on.

Here’s screenshot of the program. You’ll see why Finland is in red a little later. I clicked on the orange countries.

World map

The Map View Header

Time for the header. Here is the complete interface for the map view:

#import <Cocoa/Cocoa.h>

@protocol BNRWorldMapViewDelegate;
@interface BNRWorldMapView : NSView
@property (nonatomic, weak) id <BNRWorldMapViewDelegate> delegate;
@end // BNRWorldMapView

@protocol BNRWorldMapViewDelegate <NSObject>
- (NSColor *) worldMap: (BNRWorldMapView *) map
              colorForCountryCode: (NSString *) code;
- (void) worldMap: (BNRWorldMapView *) map
         selectedCountryCode: (NSString *) code;
@end // BNRWorldMapViewDelegate

Whoa. There’s actually a lot going on there. Let’s take it apart, piece by piece.

#import <Cocoa/Cocoa.h>

Thus pulls in the necessary interfaces for Cocoa.

@protocol BNRWorldMapViewDelegate;

I first forward-declare the protocol. It’s just promising “there is a protocol by the name BNRWorldMapViewDelegate” so that I can declare a property that references it, but I haven’t told anyone about the details yet. I could have put the whole protocol definition up here, but I like putting it later in the header file. This lets other programmers read the object’s overall programming interface before getting down into the details of the delegate or datasource protocols. You could list the protocol first, but you would need to forward-declare the map view class with @class, which can be just as confusing. I always do it this way.

@interface BNRWorldMapView : NSView

No surprises here. Just a new NSView.

@property (weak) id <BNRWorldMapViewDelegate> delegate;

This declares the map’s delegate property. It is an id, meaning that any Objective-C object, no matter its parentage, can participate. The id adopts the map view delegate protocol to force any object that is assigned to this property to declare that it conforms to that protocol. People setting their objects to be the delegate would assign their object pointers to this property, e.g. map.delegate = self; More on this in a little bit.

Notice that this property is declared weak. If you’re not using ARC, you would use assign. This is a standard technique for breaking retain cycles.

@end // BNRWorldMapView

All done with the view’s programming interface. Now for the protocol.

@protocol BNRWorldMapViewDelegate <NSObject>

This starts the protocol declaration. Protocols can adopt other protocols, and the map view delegate protocol adopts the NSObject protocol. It’s OK to be kind of confused about this. The term NSObject is used for both a class (e.g. NSAffineTransform inherits from NSObject), and for a protocol (e.g. NSTableViewDataSource adopts NSObject, this map view delegate protocol adopts NSObject). Why am I adopting NSObject? Mainly so I can call respondsToSelector: without complaint from the compiler.

- (UIColor *) worldMap: (BNRWorldMapView *) map
                   colorForCountryCode: (NSString *) code;

This is the declaration of the method that the controller classes would need to implement. Say you had a view controller whose view contains a map, and has a pointer to the map via an IBOutlet. That view controller would set itself to be the map’s delegate, and it must implement this method to supply the color for the given country code. This is a required method. Protocol methods are required by default, unless made optional.

The pattern you should use for protocol methods is to make the map object the first argument. If you’re not making maps, then the type of the first argument would be your class that has the delegate property. Inside of the world map view implementation it would call the method and pass self as the first argument. This is done so that one object can be the delegate for multiple maps. The first argument is used to disambiguate (I love that word!) between different map views.

- (void) worldMap: (BNRWorldMapView *) map
         selectedCountryCode: (NSString *) code;

This is an optional method, letting the user click on a country. The map informs the delegate that a country was selected. It’s up to the delegate to figure out what the right thing to do is. This is an optional method, so the object that’s going to become the map’s delegate doesn’t need to implement it. That’s why I’ll need to call respondsToSelector: first, and that’s why the BNRWorldMapViewColorDelegate protocol needs to also adopt the NSObject protocol.

@end // BNRWorldMapViewColorDelegate

And we’re done with the protocol declaration.

The Map View Implementation

Now to see how it’s used inside of the map view. The map view overrides drawRect: so that it can draw each of the countries. g_countryPaths is a global NSDictionary that maps country codes to NSBezierPath objects:

- (void) drawRect: (CGRect) rect {
    [[NSColor darkGrayColor] setStroke];
    for (NSString *countryCode in g_countryPaths) {
        NSBezierPath *path = [g_countryPaths objectForKey: countryCode];
        // Ask the delegate.
        <b>NSColor *fillColor = [self.delegate worldMap: self
                                  colorForCountryCode: countryCode];</b>
        if (fillColor == nil) fillColor = [NSColor whiteColor];
        [fillColor setFill];
        [path fill];
        [path stroke];
} // drawRect

drawRect: loops through the collection of countryCodes, pre-populated by the map view. For each countryCode, it fetches an outline for the country so it knows what area of the map to fill.

Now the important part:

NSColor *fillColor = [self.delegate worldMap: self
                          colorForCountryCode: countryCode];

This asks the delegate for the country code color. You can see it passing self for the first argument, and then the country code in the second argument. This is one of those techniques that makes Objective-C so much fun. The map code doesn’t care what kind of object the delegate is. It could be a view controller. It might be a table view cell. It might be some model object. It might be a distributed object that actually lives on another computer. The map code really doesn’t care, just so long as it can respond reasonably to worldMap:colorForCountryCode:, and the existence of the protocol ensures that happens or else the compiler will complain.

What about the optional method? It’s pretty easy:

- (void) mouseDown: (NSEvent *) event {
    NSPoint mouse = [self convertPoint: event.locationInWindow
                          fromView: nil];
    for (NSString *countryCode in g_countryPaths) {
        NSBezierPath *path = [g_countryPaths objectForKey: countryCode];
        if ([path containsPoint: mouse]) {
            NSLog (@"clicked in %@", countryCode);
            <b>if ([self.delegate respondsToSelector:
                          @selector(worldMap:selectedCountryCode:)]) {
                [self.delegate worldMap: self
                               selectedCountryCode: countryCode];
} // mouseDown

Here it loops through the country codes until the point lies in a country. Then it checks to make sure the delegate responds to the optional selector. If it does, tell the delegate the good news.

Quick aside – I could have checked that the delegate responds to the selector at the very beginning and completely bypassed the work of the method by using an early return. For purposes of illustration I’m assuming there’s some extra work that would happen before or after the delegate gets notified.

OK, now done with the map view! It defines a delegate, and the map view uses that delegate to figure out how to draw each country, and to tell someone else that a country was tapped on.

The class using the delegate

So, there’s now a map view that has delegation. How do you use it? Pretty easy. This program is simple, with the BNRAppController class acting as the controller. The Xcode templates would have called this BNRAppDelegate, but I thought that having too many “Delegate” names floating around would be too confusing, so I renamed it to be BNRAppController.

The AppController’s Header

Here’s the header for BNRAppController:

#import <Cocoa/Cocoa.h>

<b>#import "BNRWorldMapView.h"</b>
@interface BNRAppController : NSObject <NSApplicationDelegate,
@property (unsafe_unretained) IBOutlet NSWindow *window;
<b>@property (weak) IBOutlet BNRWorldMapView *worldMap;</b>
@end // BNRAppController

This is pretty straightforward, mostly template boilerplate. I did have to pull in the map view’s header so that the compiler can see the delegate definition, and I adopt the BNRWorldMapViewDelegate in the class interface. It’s also possible to adopt the protocol in a class extension. I’ll talk about that next time.

The AppController’s Implementation

One of the things the app controller does is keep track of what countries were clicked on so. Clicking a country toggles the color, so it has to keep track of what the “currently selected” countries are. A mutable set is perfect for holding on to the country codes of the selected countries:

@interface BNRAppController ()
@property  NSMutableSet *selectedCountries;
@end // BNRAppController

Now that the bookkeeping is out of the way, you would set the map’s delegate pointer in awakeFromNib:, and also create the set that holds on to the selected countries.

- (void) awakeFromNib {
    <b>self.worldMap.delegate = self;</b>
    self.selectedCountries = [NSMutableSet set];
} // awakeFromNib

Now implement the colorForCountryCode method. What colors should I use? It turns out that Finland has the highest per-capita density of heavy metal bands, so they get a nice bright red color for their awesomeness, while everyone else gets a mellow yellow. Selected countries get colored orange.

- (NSColor *) worldMap: (BNRWorldMapView *) map
              colorForCountryCode: (NSString *) code {
    if ([code isEqualToString: @"FI"]) {
        return [NSColor redColor];
    } else {
        if ([self.selectedCountries member: code]) {
            return [NSColor orangeColor];
        } else {
            return [NSColor yellowColor];
} // colorForCountryCode

So now when the BNRWorldMapView wants know the color of Finland, this code gets run, and fills Finland in with red. It calls this method for all of the other countries, and gets yellow or orange colors.

The final piece of code is the optional “hey the user clicked on a country” method. It gets passed the pointer to the map that’s calling the method, and the country code:

- (void) worldMap: (BNRWorldMapView *) map
              selectedCountryCode: (NSString *) code {
    if ([self.selectedCountries member: code]) {
        [self.selectedCountries removeObject: code];
    } else {
        [self.selectedCountries addObject: code];
    [self.worldMap setNeedsDisplay: YES];
} // selectedCountryCode

This is just a little work to add or remove countries from the set (a click toggles), and schedules a redraw for the map.

And that’s it! We now have an object that uses a delegate (the map view), and another object that acts as a delegate (the app controller).

What direction?

A question came up during an Advanced iOS Class where a student was unclear about which class defines the protocol and which one uses it.

Here’s the pattern: the class which is using another object, whether to get data from it like a UITableDataSource, or if it’s needing to notify another object like a UITextViewDelegate, is the class which defines the protocol and has the id <protocolname> property. It knows what data it needs, and it knows what actions it needs to tell others about, so it’s the one that’s best qualified to define the protcol.

The classes which are being used, the ones that answer the questions “Hey, you need such-and-such information? The value to use is ‘Schmoo’”, or “Oh, such-and-such happened? That’s cool! I’ll change the icon in my tab bar.” are the ones that implement the methods of the protocol, and set themselves to be the delegate of the other classes. Hopefully a little diagram helps:

Delegate illustration

Wrap up

Delegation is a very powerful pattern, allowing cooperating objects to decouple themselves from each other, while still cooperating.

Special thanks to Al MacDonald, who created the world map and posted it to Wikipedia: Also thanks to Martin Haywood who created a SVG-to-BezierPath utility. These two useful chunks of work let this posting have real code.

Speak with a nerd

Schedule a call today! Our team of nerds are ready to help

Let's Talk

Related Posts

We are ready to discuss your needs.

Stay in Touch WITH Big Nerd Ranch News