Delegates

Modified

Download

CounterDelegate

Overview

Delegates are used extensively in iOS development to send messages on events such as dismissing the keyboard.

For example, the following view controller is a UITextFieldDelegate delegate and implements the textFieldShouldReturn: message, sent when the return key is tapped on the iPhone keyboard.

@interface SomeViewController : UIViewController <UITextFieldDelegate> states that SomeViewController will implement the methods of UITextFieldDelegate.

-(BOOL) textFieldShouldReturn: (UITextField *) textField is the delegate method of the UITextFieldDelegate protocol, sent a message when keyboard return key pressed.

[textField resignFirstResponder]; dismisses the keyboard.

[text setDelegate: self]; sets the SomeViewController object as a UITextFieldDelegate delegate.

 

@interface SomeViewController : UIViewController <UITextFieldDelegate> {
     IBOutlet UITextField *text;
 }
@end
@implementation SomeViewController

- (void)viewDidLoad  {
    [super viewDidLoad];
    [text setDelegate: self];
}
-(BOOL) textFieldShouldReturn: (UITextField *) textField {
     NSLog(@"Text field contents %@ ", textField.text );
     [textField resignFirstResponder];
     return YES;
}
@end

 

-viewDidLoad method initializes the UITextField text delegate programmatically by:

[text setDelegate: self];

where self is the SomeViewController object.

Now, when the keyboard is no longer needed for typing in the text object, the UITextField class calls textFieldShouldReturn: method of SomeViewController.

The SomeViewController can also be connected as a delegate to the text field object using Interface Builder.

The mechanism used here to send delegate messages is now examined below.

 

Implementing delegates and sending messages

We can directly send a -bDidCall message to an A object by:

A * delegate = [A alloc];

[delegate bDidCall];

B object holds a reference in delegate variable to an A object (at address 100), the following sends the bDidCall message to the A object.

Delegation is commonly used for passing event notification from one class to another.

Below illustrates the use of an informal protocol to implement a delegate method, -bDidCall.

Informal protocols are used when the delegate class implements some, none or all delegate methods.

Class A registers itself as a delegate and implements the delegate method, -bDidCall.

Class B defines the delegate method -bDidCall and calls the registered delegate object, A.

After registration, calling a delegate method is essentially identical to any method call.

There are four parts to delegate use:

  1. Define an id object to reference the delegate object and delegate methods for delegates to implement.
  2. The delegate implements delegate methods and registers itself with the notifying class as a delegate.
  3. The delegate object is set in the notifying class.
  4. The notifying class calls any implemented delegate methods (though not done in this example, normally after verifying the class responds to the delegate method).

The following diagrams the classes, A and B, to illustrate the steps in the delegate execution.

Delegation
B.h
@interface B : NSObject   {
	id	delegate;			// 1
}

- (B *) initWithDelegate: (id) d;
- (void) callDelegate;
@end

@interface B (BDelegate) 
-(void) bDidCall;
@end
A.h
#import "B.h"

@interface A : NSObject
{
	B *b;
}

-(void) start;
-(id) init;

@end
B.m

#import "B.h"

@implementation B

- (B *) initWithDelegate: (id) d {          
    self = [super init];
    delegate = d;                                     // 3
    return self;
}

-(void) callDelegate{
    [delegate bDidCall];                           // 4
}
@end

A.m
#import "A.h"

@implementation A

-(void) start {
	[b callDelegate];
}

-(id) init {
	self = [super init];
	b = [[B alloc] initWithDelegate: self];    // 2
	return self;
}

-(void) bDidCall {
	NSLog(@"B did call!");
}
@end
main.m
#import <Foundation/Foundation.h>
#import "A.h";

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];

    A *a = [[A alloc] init];
    [a start];
    [pool drain];
    return 0;
}

 

@protocol

A formal delegation protocol allows the compiler to check for compliance by delegate classes.

BDelegate protocol is defined in BDelegate.h, only the method prototype is specified.

Any delegate class is required to implement the methods defined in the protocol.

A class is formally specified as a BDelegate by:

@interface A : NSObect <BDelegate>

BDelegate.h
 @protocol BDelegate

-(void) result: (int) n;

@end
Protocol Delegation
B.h
#import "BDelegate.h"

@interface B : NSObject
{
	id delegate;
}

- (void) setDelegate: (id) d;

- (void) factorial: (int) n;

@end
A.h

#import "B.h"
#import "BDelegate.h"

@interface A : NSObject <BDelegate>
{
	B *b;
}

-(void) start;
-(id) init;
-(void) result: (int) n;

@end
B.m
#import "B.h"

@implementation B

- (void) setDelegate: (id) d {
	delegate = d;
}

-(void) factorial: (int) n {
	int f = 1;
	for(int i=1; i<=n; i++)
		f = f * i;

	if ([delegate respondsToSelector:@selector(result:)])  
		[delegate result: f];
} 
@end
A.m
#import "A.h"

@implementation A

-(void) start {
	[b factorial: 5];
}

-(id) init {
	self = [super init];
	b = [B alloc];
	[b setDelegate: self];
	return self;
}

-(void) result: (int) n {
	NSLog(@"B did call! %d ", n);
}
@end
#import <Foundation/Foundation.h>
#import "A.h";

int main (int argc, const char * argv[]) {
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
	
    A *a = [[A alloc] init];

    [a start];
    [a release];

    [pool drain];
    return 0;
}

Problems

The above example works but has one glaring problem - B can have only one delegate at a time - and has no way to remove a delegate.

Below, B has been modified to accommodate multiple delegates.

Protocol Delegation
B.h
#import "BDelegate.h"

@interface B : NSObject
{
	NSMutableArray * delegates;
}

- (void) setDelegate: (id) d;

- (void) factorial: (int) n;

@end
B.m
#import "B.h"

@implementation B

- (void) setDelegate: (id) d {
    if( !delegates )  delegates = [[NSMutableArray alloc] init];
    [delegates addObject: d];
}

-(void) factorial: (int) n {
     int f = 1;
     for(int i=1; i<=n; i++)
	f = f * i;
    
    for( NSObject *delegate in delegates) 
          if ([ (id) delegate respondsToSelector:@selector(result:)])  
                 [ (id) delegate result: f];
} 
@end

 

Counter delegate

The following is a simple counter using a protocol to specify the methods that must be implemented.

In this case, for simplification, only a single delegate is permitted at a time.

 

protocol defines the methods that must be implemented by a CounterDelegate:

-(void) setcount: (NSString *) s;

Counter delegate protocol
@protocol CounterDelegate
    -(void) setcount: (NSString *) s;
@end
 

Counter is the class that counts off the seconds and performs call-backs to the CounterDelegate method setcount().

A NSTimer calls timerCallBack at one second intervals which then performs a call-back to the CounterDelegate method setcount().

It is important to note that NSTimer objects execute on the main-loop so that call-backs to the CounterDelegate method setcount() are on the main-loop, allowing UI objects to be accessed.

Counter interface and implementation
@interface Counter : NSObject{
    id              delegate;
    NSTimer * timer;
    int             seconds;
}
  -(id) initWithDelegate: (id) delegate;
  -(void) count;
  -(void) reset;
@end

@implementation Counter

-(id) initWithDelegate: (id) d {
    delegate = d;
    seconds = 0;
    return self;
}

-(void) count {
    timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self
                  selector: @selector(timerCallBack:) userInfo: nil repeats: YES];    
}

-(void) reset {
    seconds = 0;
}

- (void)timerCallBack:(NSTimer *)timer{
    seconds++;
    if ([delegate respondsToSelector:@selector(setcount:)]) 
          [delegate setcount: [NSString stringWithFormat:@"%d", seconds]];
}
@end
 

CounterViewContoller is a CounterDelegate that implements the method setcount().

During startup, it initializes self as a CounterDelegate  by:

counter = [[Counter alloc] initWithDelegate: self];

Counter view controller
#import <UIKit/UIKit.h>
@interface CounterViewController : UIViewController <CounterDelegate> {
    IBOutlet UILabel * count;
    Counter *counter;
}
   -(void) setcount: (NSString *) s;
@end

@implementation CounterViewController

-(IBAction) reset: (id) sender{
    [counter reset];
}

- (void)viewDidLoad {
    [super viewDidLoad];
    counter = [[Counter alloc] initWithDelegate: self];
    [counter count];
}

-(void) setcount: (NSString *) s {
    [count setText: s];
}
@end

Question

Memory management was ignored for the sake of clarity.

What should be added to prevent memory leaks?

Are there any potential dangling references?

Should any references be retained? Note that NSTimer performs a retain.

-(id) initWithDelegate: (id) d {
    delegate = d;
    seconds = 0;
    return self;
}

When delegate thread should be used

NSTimer executes on the main-thread which happened to be the right thread for the call-back.

Suppose the call-back needed to be back to the delegate's thread which may or may not be the same as the NSTimer?

The logical solution is to always perform the call-back on the delegate's thread as re-implemented below.

Note that the main-event loop thread calls:

-(id) initWithDelegate: (id) d

so currentThread is the main-event loop thread.

The only change to Counter class is initializing delegateThread to reference the currentThread, and call-back the delegate's setcount() on delegateThread.

Counter interface and implementation
@interface Counter : NSObject{
    id        	delegate;
    NSTimer * timer;
    int       	seconds;
    NSThread *delegateThread;
}
-(id) initWithDelegate: (id) delegate;
-(void) count;
-(void) reset;
@end

@implementation Counter

-(id) initWithDelegate: (id) d {
    delegateThread = [NSThread currentThread];
    delegate = d;
    seconds = 0;
    return self;
}

-(void) count {
    timer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target: self 
                  selector: @selector(timerCallback:) userInfo: nil repeats: YES];	    
}

-(void) reset {
    seconds = 0;
}

- (void)timerCallback:(NSTimer *)timer{
    seconds++;
    if ([ delegate respondsToSelector:@selector(setcount:)]) 
        [ delegate performSelector:@selector(setcount:) onThread: delegateThread 
                        withObject: [NSString stringWithFormat:@"%d", seconds] waitUntilDone:NO];
}
@end

One final point. Call-back on the delegate thread nearly always makes sense but is necessary when the call-back is performed by thread different than the delegate's.

For an example, see NSXMLParser.