Tutorial: How to use setDelegate: correctly


#1

I’m a total cocoa programming beginner, and I became very frustrated by the section on Delegates after trying to do the challenge “Make a Delegate”. In the Delegates section (on p.96), the author teaches the reader how to setup a delegate: you send the message setDelegate: to an object. As a result, I came up with the following code for the challenge (I decided to dispense with retain counts/memory management until after I got my app working):

MyDelegate.h:

[code]#import <Cocoa/Cocoa.h>

@interface MyDelegate : NSObject {

IBOutlet NSWindow* window;

}

  • (id)init;
  • (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize;

@end[/code]

MyDelegate.m

[code]#import “MyDelegate.h”

@implementation MyDelegate

  • (id) init
    {
    if (![super init]) {
    return nil;
    }

    [window setDelegate:self];

    return self;
    }

  • (NSSize)windowWillResize:(NSWindow *)sender
    toSize:(NSSize)frameSize
    {
    frameSize.height = 2 * frameSize.width;
    return frameSize;
    }

@end
[/code]
In Interface Builder(IB), I dragged an Object from the Library onto MainWindow.xib, and in the Identity Inspector I entered MyDelegate for the class name. Then I ctrl-clicked on the MyDelegate instance in MainWindow.xib, and I dragged a connection from the ‘window’ outlet to the app’s main window.

Then I saved everything, clicked build&run, but I could resize the window to any size, i.e. the window size would not maintain a 2:1 ratio. Why didn’t setDelegate: succeed in setting the delegate for the app window?

There is a section titled ‘Common Errors in Implementing a Delegate’ in the book (on p. 107), and it says one of the errors is “Forgetting to set the delegate outlet”. Whaaa??! I flipped back to the Delegate section of the book, and I carefully looked at the example, and in the example the author never uses IB to create a connection for the delegate.

After fooling around with my code and messing around with connections in IB, I came up with some working code:

MyDelegate.h

[code]#import <Cocoa/Cocoa.h>

@interface MyDelegate : NSObject {

}

  • (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize;

@end
[/code]

MyDelegate.m

[code]#import “MyDelegate.h”

@implementation MyDelegate

  • (NSSize)windowWillResize:(NSWindow *)sender
    toSize:(NSSize)frameSize
    {
    frameSize.height = 2 * frameSize.width;
    return frameSize;
    }

@end[/code]
In IB, I ctrl-clicked on the app’s main window (anywhere in the title bar except on top of the icon) to get a list of the window’s outlets, and I dragged from the ‘delegate’ outlet to the MyDelegate instance in MainWindow.xib. That’s the only connection that was necessary. Then the app worked, and the window was constrained to a 2:1 ration when I resized it.

But why didn’t the code using setDelegate: work? When a designated event occurs, all the window does is grab the delegate object and uses it to call the delegate methods. So setting the delegate object for the window is all that should be needed for things to work correctly. I searched the internet high and lo, and I couldn’t find any information about why setDelegate: wouldn’t work in my original code. Until I found this:

[quote]An awakeFromNib message is sent to each object loaded from the archive, but only if it can respond to the message, and only after all the objects in the archive have been loaded and initialized. When an object receives an awakeFromNib message, it is guaranteed to have all its outlet instance variables set.
[/quote]
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Protocols/NSNibAwaking_Protocol/Reference/Reference.html

Huh?!! You mean it’s NOT guaranteed that the objects pointed to by my class’s instance variables exist???

[quote]During the instantiation process, each object in the archive is unarchived and then initialized with the method befitting its type…

Once all objects have been instantiated and initialized from the archive, the nib loading code attempts to reestablish the connections between each object’s outlets and the corresponding target objects. If your custom objects have outlets, an NSNib object attempts to reestablish any connections you created in Interface Builder…

Finally, after all the objects are fully initialized, each receives an awakeFromNib message.[/quote]

That means that inside an init() method, you can’t send messages to (i.e call methods on) other objects–including those objects that are pointed to by your class’s instance variables. During the initialization phase, i.e. in your init() methods, you have to assume that no other objects exist (unless you specifically created those objects inside your init() method). As a result, if you want to call methods on other objects, you have to do it in awakeFromNib at such time when all the other objects in your app are guaranteed to exist.

I modified my code to call setDelegate: in an awakeFromNib method, and now the code works as expected, and the window size is constrained to a 2:1 ratio when resizing:

MyDelegate.h

[code]#import <Cocoa/Cocoa.h>

@interface MyDelegate : NSObject {

IBOutlet NSWindow* window;

}

  • (void)awakeFromNib;
  • (NSSize)windowWillResize:(NSWindow *)sender toSize:(NSSize)frameSize;

@end
[/code]

MyDelegate.m

[code]#import “MyDelegate.h”

@implementation MyDelegate

  • (void)awakeFromNib
    {
    [window setDelegate:self];
    }

  • (NSSize)windowWillResize:(NSWindow *)sender
    toSize:(NSSize)frameSize
    {
    frameSize.height = 2 * frameSize.width;
    return frameSize;
    }

@end
[/code]
In IB, I ctrl-clicked on the MyDelegate instance in MainWindow.xib, and then I dragged a connection from the ‘window’ outlet to the app window. Then I ctrl-clicked on the app window’s title bar, and I deleted the connection from the ‘delegate’ outlet to the MyDelegate instance.

The detailed explanation of why my first attempt to use setDelegate: did not work goes like this. The NSWindow* window instance variable in my class had not been assigned a value yet when the init() method was executing. As a result, this line:

was equivalent to:

and as the author mentions (on p. 40):

That means the app window never got the message to set the delegate. When my init() method was executing, Cocoa was still initializing all the different windows and objects for the app. Personally, I would rather see my code fail when I try to call a method on an object that doesn’t exist.

Looking more closely at the example in the book that uses setDelegate:, the example does call setDelegate: inside of init(), but the reason the example works is because the object that the author calls setDelegate: on was created in the init() method, so that object exists.

The takeaway is that there are two ways you can setup a delegate for an object:

  1. Using IB: Drag a connection from an object’s ‘delegate’ outlet to another object that will serve as the delegate.
  2. Programmatically: In the delegate object’s class, implement the awakeFromNib method and call setDelegate: inside awakeFromNib.

It would have been nice if the author had mentioned the very important fact that when you call setDelegate: in your code, you must ensure that the object that you send the setDelegate: message to exists–and inside init() none of the controls(buttons, text fields, etc.) in your window exist yet. So you if you want to call setDelegate: on one of your window’s controls, you have to do it inside awakeFromNib. Instead of the author blabbing on about KITT the super car, I would have much preferred reading the important information about the initialization process I quoted above.

Conceptually, a datasource is no different from a delegate: when a special event occurs, a control grabs the object designated as its datasource and calls the datasource methods using the datasource object. Classes like NSTableView specify the methods they expect a datasource to implement, just like they specify methods they expect a delegate to implement. To discover what those methods are, you can look through the header file for the NSTableView (alt-double click on NSTableView in your code, then click the ‘h’ in the upper right corner). You’ll find something like this:

[code]@protocol NSTableViewDataSource
@optional

/* Required Methods
*/

  • (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView;
  • (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row;[/code]

You’ll see a different set of methods for that an NSTableView expects its delegate to implement.

If the names ‘delegate’ and ‘datasource’ are too intimidating, you should imagine a TableView that has two instance variables, one that points to an object known as a ‘puppy’ and the other that points to and object known as a ‘kitten’. When a designated event occurs in the TableView, the TableView grabs the ‘puppy’ object and uses it to call a set of methods that are specified in the TableView class, so the ‘puppy’ better define those methods. Likewise, when another designated event occurs, the TableView grabs the ‘kitten’ object and uses it to call another set of methods that are specified in the TableView class, so the ‘kitten’ better define those methods.