Thursday, May 7, 2009

iPhone tutorial: Creating an "exitable" user interfaces

Many iPhone applications have a single user interface which you cannot "exit" - it's displayed all the time. It can still be quite complex with multiple level navigation controllers and table views, multiple tabs in a tab controller, or even a combination of a tab bar controller and navigation controllers, but still, you see the tab / navigation bar all the time. There is no exit.

The XCode "Utility Application" template, the iPhone stocks and weather applications are some examples of a different breed of applications. They have a "primary screen" which displays some information and might even allow you to interact with it. When you need to change a setting or something similar you bring up the "secondary screen", which is a traditional iPhone user interface with all the well known components. You can say that this kind of application has multiple interfaces, which you can switch between or exit from.

In this tutorial, we're going to create a simple multi-interface application. It will consist of a primary screen with a single button on it which takes us to a secondary screen with a tab bar controller with a single tab. The view of the single tab will also have a single button which "exits" the interface and takes us back to the main screen.

Start XCode and choose "File/New project" from the menu and create a "Window-Based Application" called "Multi". This will create a project with the following structure:

Multi
  Classes
    MultiAppDelegate.[hm]
  Resources
    MainWindow.xib

Primary screen

To keep the project small, we'll use the application delegate to implement the primary screen. The delegate will also function as the central point of our application which switches between the primary and secondary screen. We will also "cheat" a little and won't use a view controller for this screen because it is so simple and the main intent with this tutorial is to demonstrate how to "exit" an user interface. The only thing the primary screen will have to handle is to detect that a button was pressed so that it can switch between the primary and secondary screen.

MultiAppDelegate.h

@interface MultiAppDelegate : NSObject {
UIWindow *window;
UITabBarController *tabBarController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet UITabBarController *tabBarController;

-(IBAction)buttonPressed:(id)sender;

Edit the file to make it look like above. We added the 'tabBarController' pointer and made it into a property and marked it with IBOutlet to make it accessible from Interface Builder (IB). We'll use this to keep track of the tab bar controller we will create. We also added an IBAction called 'buttonPressed:'. Notice the colon (:) at the end of the name? That's Objective C's way of saying that this method takes one argument. Since we're dealing with an iPhone "action" this argument is the sender of the action, which in our case will be an UIButton.

MultiAppDelegate.m

Instruct the compiler to create the accessors (set/get) for our 'tabBarController' property by editing the top of the file to make it look like this:

@synthesize window;
@synthesize tabBarController;

Implement the IBAction method by editing the bottom of the file to make it 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];
}
}
@end

The first line converts the generic 'sender' pointer into a specific pointer 'button' of the type UIButton. After that we add some diagnostic logging so that we can see (in the XCode console window) that the method has been called. Then comes an if-clause that checks the title of the button that was pressed. If it was "TabBar" we make the 'tabBarController' appear by adding its view to our window. If it was "Done" we remove the 'tabBarController' view from its superview by, quite logically, calling removeFromSuperview and thus make it disappear.

Did you notice that we didn't do anything in 'applicationDidFinishLaunching'? That's where we usually makes our user interface appear by adding a subview to our window, but now that won't happen until we press a button.

Secondary screen

As we said initially, the secondary screen will consist of a tab bar controller with a single tab, or "item" as they are called in the iPhone, and thus a single view controller. Create the view controller's source code files by selecting the "Classes" group in XCode's "Groups & Files" section and choose "File/New file" from the menu. Select "UIViewController Subclass" and name the file Item1ViewController.m and ensure that "also create h-file" is enabled. This will create the following files.

Item1ViewController.h

@interface Item1ViewController : UIViewController {

}

-(IBAction)buttonPressed:(id)sender;

The only change we made to this was to add the IBAction 'buttonPressed:' (once again notice the colon which indicates that this method takes one argument). This action will be generated by our "exit"-button which will take us back to the primary screen.

Item1ViewController.m

At the top of the file, add a a new import-statement under the existing import-statement:

#import "MultiAppDelegate.h"

At the end of the file, just before the @end marker add the following:

-(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];
}
}

This method starts out in the same way as the identically named method in MultiAppDelegate.m; we create a 'button' pointer from the generic 'sender' pointer and log a diagnostic message to the XCode console window to indicate that the method has been called. We then check if the title of the pressed button was "Done" and if it was we do some magic!

The intention of this magic is to get hold of a pointer to our MultiAppDelegate object. We do this by first getting hold of a pointer to our UIApplication object by calling a "class-method" of the UIApplication class called 'sharedApplication'. Once we have this pointer we can get hold of our UIApplication's delegate by calling 'delegate' on it. This, in turn, gives us another pointer - pointing to our MultiAppDelegate object - and since we know that our delegate is of the type MultiAppDelegate we cast the delegate pointer to that type. After this, we have a pointer to our MultiAppDelegate object in 'multiAppDelegate' - neat, huh?

After that we can call MultiAppDelegate's method 'buttonPressed:' just as if we have had setup a property for it and connected the MultiAppDelegate object to it in IB. This would have been impossible in IB since we modularised the interface into two xib-files instead of one, so thanks Apple for making this magic possible ;)

But why do we want to call 'buttonPressed:' in MultiAppDelegate when we could handle the buttonPressed action here in Item1ViewController? That's because only MultiAppDelegate has the possibility to remove the tab bar controller. Here in Item1ViewController, we're "one level down" and doesn't really know that we are inside a tab bar controller. Thus we need to "step up one level" where we have a better overview of our application and can see clearly that a tab bar controller actually exists. When going to UIApplication we actually do more than step up one level, we actually go to the absolute top of our application where we have a complete overview of everything that's going on. This makes UIApplication, or as in this case, it's delegate the perfect "central controlling point" of the application.

Speaking of this, I stumbled across an old discussion thread debating how to do "central control" like this. It's quite informative, so put it on your reading list.

Letting the "Done"-button inform both Item1ViewController and MultiAppDelegate instead of just MultiAppDelegate gives Item1ViewController a chance to "clean up and finish" before MuliAppDelegate removes it from its view.

Designing the interfaces

Now that we have created the source code for our classes, it's time to tie everything together in Interface Builder (IB) as well as add the buttons, tab controllers, etc. 

MainWindow.xib

Double-click on Resources/MainWindow.xib in XCode to start IB and make it load the xib-file. As always, switch to "hierarchical view mode" in IB by pressing the middle button above the text "View mode" in the MainWindow.xib window.

Button

We start by adding the button which will make the secondary user interface appear. If you're really observant, you might have noticed that the title of this button has to be "TabBar" since that's what we're comparing against in the 'buttonPressed:' method in MultiAppDelegate. So, bring up the Library window (CMD-L) and drag a "Rounded Rect Button" from the "Inputs & Values" section in the Library window and drop it on top of the Window-object in the MainWindow.xib window. The button should appear under the Window-object, indented one level and a small arrow should be added to the left of the Window-object to indicate that it "contains something".

Double-click on the "Rounded Rect Button" object to bring up the edit window. The button should appear in the center of the window. Now double-click the button in the edit-window and enter the text "TabBar". Return to MainWindow.xib and CTRL-drag from "Rounded Rect Button" to "Mutli App Delegate" and release the mouse button. A window called 'Events' should pop-up and allow you to select 'buttonPressed:' (once again, notice the colon at the end). Select it, and you have made the "Multi App Delegate" object the target of the button's default action. This default action is sent whenever the button is pressed.

Tab Bar Controller

Now we should add the tab bar controller which will function as our secondary user interface, so drag a "Tab Bar Controller" from the "Controllers" section of the Library window to the MainWindow.xib window and drop it at the end of the list. Immediately expand the hierarchy under the controller by pressing the small arrow which is located to the left of the "Tab Bar Controller" object in the list in MainWindow.xib. See that it contains two view controllers? That's because a tab bar controller by default has two tabs, sorry, items. In this example we should only use one, so select the second view controller and press backspace.

Our application delegate should handle this tab bar controller, so CTRL-drag from the delegate to the tab bar controller and connect the 'tabBarController' outlet/property.

Item1ViewController.xib

To make things a bit more modular, we should create a separate xib for the view controller, or actually its contents; the views, buttons, etc. Do this by choosing "File/New" from the IB menu and select the "View" template. Start by saving the file by choosing "File/Save as" from the menu. Navigate to the directory containing your project files and save the file as "Item1ViewController" (the xib-extension will be added automatically). IB will bring up a window asking if you want to add the file to your project. We do so check the checkbox and click the Add-button.

Go back to XCode and verify that the file has appeared in our project. It should be located at the end of the list of files under the "Multi" group - just above the "Targets"-group. We want it in the Resources group, so drag the xib-file into it. Build the project (CMD-B) and XCode prompts you to save the changes - do that.

Return to IB by double-clicking on Resources/MainWindow.xib. Locate the MainWindow.xib window in IB and select the "Selected View Controller" object. Press CMD-1 to bring up the Attributes tab of the Inspector window and choose Item1ViewController from the "NIB Name" drop-down menu. This tells IB that the contents of this view controller should be loaded from that file. Thus, we shouldn't add any contents to the view controller here in MainWindow.xib. 

However, if you expand the view controller by pressing the small arrow to the left of it, you'll see that it already contains a "Tab Bar Item" but that's ok because that object is a bit special. It's used to control the appearance of this view controller on the tab bar, so you could say that it's part of the configuration for the tab bar controller rather than the view controller.

As the final modularisation step, select the "Selected View Controller", press CMD-4 and select "Item1ViewController" from the Class drop-down menu so that IB will create our UIViewController subclass instead of a generic UIViewController.

Simulated metrics

Locate the Item1ViewController.xib window in IB and select the "View" object. This view will be displayed "inside" a tab bar controller and will thus not have access to the full display of the iPhone, since the bottom part of the display will be covered by the tab bar controller. If we would have designed this view in MainWindow.xib instead of modularising it into its own xib-file this would have been handled automatically for us, but now we need to "simulate" it. Therefore, after selecting the "View" object, press CMD-1 and you'll see that there is a section called "View Simulated Bar Metrics", which is exactly what we're looking for. Choose "Tab Bar" from the "Bottom Bar" drop-down menu. If you double-click on the "View" object in the Item1ViewController.xib window you'll see that a an empty black bar now has appeared at the bottom of the edit-window. This helps us avoid this area when placing buttons, etc, since they otherwise would be obscured by the tab bar.

"Exit" button

While we have the edit-window at the front, we should take the opportunity to add our "Exit" button to the view. This button should tell the application that we're done using the secondary user interface and want to return to the primary. If you paid close attention to the source code of the 'buttonPressed:' methods in MultiAppDelegate.m and Item1ViewController.m you probably noticed that the title of this button should be "Done" since that's what we compare against. So drag a "Rounded Rect Button" from the "Inputs and Values" section of the Library Window (CMD-L) in IB  into the edit-window of the "View" object (the one with the simulated tab bar at the bottom).  After you have placed the button, double-click it and enter the text "Done".

File's owner

Item1ViewController.xib will be used to initialise our class ItemViewController, so we should change the class of the "File's owner" object to that class. Do this by selecting "File's owner", press CMD-4 and choose "Item1ViewController" from the Class drop-down menu. After selecting this, you should see that our action 'buttonPressed:' appears in the "Class actions" section just below the Class drop-down menu. Selecting the class of the "File's owner" thus gives us access to all the stuff we have defined for the class.

That's good, because we need to connect the "View" object to our view controller's view-property, so CTRL-drag from "File's owner" to "View" and select 'view' in the window that pops-up.

We also need to make "File's owner", or actually our Item1ViewController, the target of the "Done-button's" default action, so CTRL-drag from "Rounded Rect Button" to "File's owner" and select 'buttonPressed:' in the window that pops up.

Test run

Save everything in IB (CMD-S) and go back to XCode to build and run (CMD-Return). The application should appear in the iPhone simulator shortly and you should see the primary screen consisting of a white screen with a button called "TabBar". Press it and the secondary screen should appear, containing a tab bar controller with a single item should appear. In the view of the first item should be a button called "Done". Press it and you should return to the primary screen.

If you take a look in XCode's console window (CMD-R), you should see something like this:

[Session started at 2009-05-08 16:39:14 +0200.]
2009-05-08 16:39:20.278 Multi[28085:20b] MultiAppDelegate: buttonPressed:sender=0x525800, title=TabBar
2009-05-08 16:39:22.110 Multi[28085:20b] Item1ViewController:buttonPressed:sender=0x536e30, title=Done
2009-05-08 16:39:22.111 Multi[28085:20b] MultiAppDelegate: buttonPressed:sender=0x536e30, title=Done

Notice how the "Done"-button generates two entries in the console log. The first is from the 'buttonPressed:' method in Item1ViewController and the second is from 'buttonPressed:' in MutlAppDelegate since Item1ViewController calles MultiAppDelegate after it has processed the action.

No comments:

Post a Comment