Saturday, May 9, 2009

iPhone tutorial: "Exitable" user interface, part 2 - delegation using a protocol

Here is another unplanned "part 2". After writing the last post I felt that the "piece of magic" I used to make the tab bar controller disappear so that we would return to the "primary screen" was a bit too quick and dirtyish. If you don't remember what piece of magic I'm talking about it was this piece of code:

Multi2AppDelegate *multiAppDelegate = (Multi2AppDelegate *)[[UIApplication sharedApplication] delegate];
[multiAppDelegate buttonPressed:sender];

Just that I refered to it as magic probably made you think that it wasn't completely kosher. In the iPhone and Objective C world you usually solve a problem like this using "delegation". If the word delegation sounds familiar to you, it's probably because most iPhone applications have a file where the word delegate appears in the name of the file. This, in turn, is because the UIApplication object has a 'delegate' property which it uses to inform another object of certain important things that are happening in the application.

Well, we have a similar situation in our little application. We want to signal that we want to exit the secondary user interface - the tab bar and its view controllers - when the user presses the "Done"-button. This is also an important thing that happens and that another object want to know about. In our case we want to know about it in order to be able to make the secondary interface disappear so that we can return to the primary interface.

Since the situations are similar, perhaps we should use "delegation" here as well? If we check the API docs for the UIApplication class in XCode we see that the 'delegate' property is defined like this:
@property(nonatomic, assign) id<UIApplicationDelegate> delegate

The type of the property is 'id', which is the Objective C way of saying that we want a pointer to a generic object. Notice that we don't have to use an astersik character (*) in the specification even though this is a pointer - that's a Objective C convenience thing. But what is the UIApplicationDelegate stuff between the less than and greater than characters? That's the name of a Objective C "protocol", which, simply put, is a set of methods that a class has to implement. So the strange looking declaration for 'delegate' can be interpreted as "delegate is a pointer to an object which implements the UIApplicationDelegate protocol".

Item1ViewController.h

We want to define a 'delegate' property for our Item1ViewController class so that someone interested in knowing what's happening in the view controller can put a pointer to itself in the 'delegate' property. Edit the file so that it looks like this (some things are omitted here):
@protocol Item1ViewControllerDelegate
-(void)doneButtonPressed;
@end

@interface Item1ViewController : UIViewController {
id<Item1ViewControllerDelegate> delegate;
}

@property (nonatomic, retain) IBOutlet id<Item1ViewControllerDelegate> delegate;

-(IBAction)buttonPressed:(id)sender;

@end


In the beginning of the file we define a protocol called 'Item1ViewControllerDelegate', which contains a single method declaration for a method called 'doneButtonPressed'. Apparently, the only thing we think that someone might be interested to know about us is that the "Done"-button has been pressed. After this we declare a variable called 'delegate' which should point to an object implementing the protocol we just defined. We also make this variable into a property and mark it with IBOutlet so that we can manipulate it from Interface Builder (IB). The rest is unchanged from before.

Item1ViewController.m

Since we added a property declaration in the h-file, we need to implement in the m-file. We do this by adding the following to the top of the file, right after the @implementation-line.

@synthesize delegate;

After this, we should re-write the 'buttonPressed' method to make it look like this:

-(IBAction)buttonPressed:(id)sender {
UIButton *button = (UIButton *)sender;
NSLog(@"Item1ViewController:buttonPressed:sender=%p, title=%@", sender, button.currentTitle);
if ( [button.currentTitle compare:@"Done"] == NSOrderedSame ) {
// Multi2AppDelegate *multiAppDelegate = (Multi2AppDelegate *)[[UIApplication sharedApplication] delegate];
// [multiAppDelegate buttonPressed:sender];
[delegate doneButtonPressed];
}
}

We have commented out the two lines containing the UIApplication magic and replaced it with a single line which calls the 'doneButtonPressed' method of the 'delegate' object (pointer). Instead of calling a method of a specific object - the one that the UIApplication's 'delegate' property points to - we call the method of an unspecified object - the one that our 'delegate' property points to. This gives us much more freedom in choosing which object that will react to the pressing of the "Done"-button. Instead of having to use the UIApplication delegate object we can use any object.

Using "callbacks" like this is often referred to as loose coupling, meaning that the sender and receiver of the message (caller and callee of the method) doesn't need to know that much about eachother. The only thing they do need to know about is that both implement the same protocol (or interface).

If you wonder how we dared calling a method on the 'delegate' object before checking if it was set, that's because it's allowed to send messages to (call methods of) the "nil object".

MultiAppDelegate.m

The whole reason of introducing delegation in Item1ViewController was to be able to implement the "Done"-button functionality in any object instead of in the UIApplication's 'delegate' object. But, in order to keep this part of the tutorial short, we will use it anyway, so edit the 'buttonPressed' method and add a new method called 'doneButtonPressed'. After you edit the file, it should look like this:

-(IBAction)buttonPressed:(id)sender {
UIButton *button = (UIButton *)sender;
NSLog(@"MultiAppDelegate: buttonPressed:sender=%p, title=%@", sender, button.currentTitle);
if ( [button.currentTitle compare:@"TabBar"] == NSOrderedSame ) {
[window addSubview:[tabBarController view]];
// } else if ( [button.currentTitle compare:@"Done"] == NSOrderedSame ) {
// [[tabBarController view] removeFromSuperview];
}
}

// Item1ViewControllerDelegate protocol implementation

-(void)doneButtonPressed {
NSLog(@"doneButtonPressed");
[[tabBarController view] removeFromSuperview];
}

In 'buttonPressed', we have commented out the quick and dirty version of the code which removed the tab bar controller view if a button named "Done" was pressed. We have replaced this with an implementation of the Item1ViewControllerDelegate protocol, which consists of the single method 'doneButtonPressed'. We now removed the tab bar controller in this new method instead.

MainWindow.xib

Since we have defined the MultiAppDelegate and Item1ViewController objects in the same IB-file, it becomes very easy to make the 'delegate' property of the ItemViewController point to MultiAppDelegate. Just CTRL-drag from the view controller to the app delegate and select the 'delegate' outlet in the window that pops up.

That's it, we have replaced our quick and dirty solution with a very well behaved delegation implementation! Beautiful, and very professional looking, huh? Delegation can actually be used for other stuff than informing another object of what's going on. It can also be used to control another object, for example to instruct a generic object of what to display. That's how the 'dataSource' in an UITableView works, but that's outside the scope of this tutorial ;)

3 comments:

  1. My friend, this is awesome. I spent all day yesterday (literally) trying to get an app to have a start screen with two buttons that were supposed to send the user to a "setting" screen and a "game". I found Nick Myers multiple Nib tutorial, but putting all this functionality into the delegate didn't feel right. Now with your addition above, I get how I can create a "masterViewController" object that has a "switchView" method that's called via delegation for each of the individual view controllers. That feels right.

    ReplyDelete
  2. Great that you found the tutorial useful! I also had a very hard time trying to find a way of accomplishing this that felt right. I don't know if this is how the Mac and Objective C pros would do it, but it works for me.

    I actually had exactly the same reason as you for learning this, since I'm also working on a game that has two completely different interfaces - a game interface and a menu interface.

    I'm trying to do my game "by the (iPhone) book" so to speak and therefore try to do as much as possible in Interface Builder, have a view controller subclass per "screen"/"page", have a single xib per view, etc. It all adds up to *a lot* of files, most of them containing just a few lines of code, but it's very modular ;)

    ReplyDelete
  3. if ( [button.currentTitle compare:@"Done"] == NSOrderedSame )
    should be written as
    if ( [button.currentTitle isEqualToString:@"Done"] )

    ReplyDelete