Tuesday, April 28, 2009

iPhone tutorial: Adding ui controls to a view and handle the events/actions

This tutorial could have been called "Navigation Controller from Scratch, part 2" since we're going to continue from the point where we ended that tutorial, but I decided against it since I wanted a more representative title.

Originally, I didn't plan to write a part two, but I thought it was pretty boring to leave you with just a navigation controller and some view controllers. Therefore I thought, why not add some "controls" to make things more interesting? If you wonder what a control is, it is a collective name for buttons, sliders, etc. that are "used to convey user intent to the application" as the API docs for UIControl states it. Speaking about the API docs for UIControl, that document contains some valuable information for understanding how controls "communicate" with the applicaiton - especially the "The Target-Action Mechanism" section - so go ahead and read it right now if you want to.

So, start by loading the "WinNav" project that we created in the last part into XCode. The first thing we're going to do is to undo some things we did in the first part. Double-click on Resources/MainWindow.xib to start Interface Builder (IB) and load the xib-file into it. As always, remember to check that the MainWindow.xib file in IB is in hierarchical view mode by clicking the middle  button above the text "View mode" in the upper left corner of the window.

Select "Win Nav App Delegate" and CTRL-click on it. This should pop up a window showing all the outlets, etc. of the object. Scroll down to the "Received Actions" section and disconnect the 'next' and 'next2' actions by pressing the small "x" to the left of the "Rounded Rect Button".

If you are really observant you probably wondered why we never used the 'button' and 'button2' outlets in the last part, although we took ourselves the time to connect them. We did this to demonstrate how to make it possible, if required, to get a reference to an object created in IB by letting IB assign a value to a pointer that we defined in the source code.

WinNavAppDelegate.h

In this part we will need references to IB objects since we're going to play with controls that send "actions" to us (calls methods) and it's always nice to see exactly which object it was that sent us an action. So how do we find out which control that sent us an event? It's pretty simple since controls can send three "types" of actions to us, or actually provide 0, 1 or 2 arguments when calling the method associated with an event:

- (void)action
- (void)action:(id)sender
- (void)action:(id)sender forEvent:(UIEvent *)event

As you can see, one of the arguments is called 'sender' and that is actually the pointer to the object which sent the action. You probably remembered that we defined two actions - 'next' and 'next2' - in the last part using the following lines in WinNavAppDelegate.h:

- (IBAction)next;
- (IBAction)next2;

Neither of these take any arguments so we shoud edit them to make them look like this:

- (IBAction)next:(id)sender;
- (IBAction)next2:(id)sender;

Now we are defining that we are ready to receive two different actions, each taking one argument; the sender of the action.

WinNavAppDelegate.m

Since we changed the definition in the h-file we need to do the corresponding change in the m-file, so open up WinNavAppDelegate.m, scroll down to the 'next' and 'next2' methods and edit them to make them look like this:

- (IBAction)next:(id)sender {
NSLog(@"next: sender=%p, button=%p, button2=%p", sender, button, button2);
[navigationController pushViewController:viewController2 animated:YES];
}

- (IBAction)next2:(id)sender {
NSLog(@"next2: sender=%p, button=%p, button2=%p", sender, button, button2);
[navigationController pushViewController:viewController3 animated:YES];
}

Now the method implementation also take one argument, 'sender', and we have also added a call to NSLog() to log some diagnostic output to the console which will make us understand things better. What the NSLog() calls does is to indicate which method that is executing ('next:' and 'next2:' in the beginning of the string), then output the value of the 'sender' argument (a pointer, therefore the %p format specifier) and after that output the value of the 'button' and 'button2' pointers (%p format once again).

Build the project (CMD-B) to verify that there are no compilation errors, then double-click on Resources/MainWindow.xib to go back to IB.

Reconnecting things

Since we disconnected the 'next' and 'next2' actions during the cleaning up phase in the beginning of this post, we need to reconnect them to make things work again. But hey, why did we disconnect them, if we're now going to reconnect them. Seems kind of stupid, doesn't it? Yes, it does, if it wasn't for the fact that we changed the IBAction defintions in WinNavAppDelegate.h. Therefore, CTRL-drag from "Round Rect Button (Second)" to "Win Nav App Delegate" to connect it to the 'next:' event in the small window that pops up. If you are super-observant you probably noticed the colon (":") at the end of the event name 'next:'. That colon indicates that this event/action takes an argument. That's the 'sender' argument we added in the IBAction definition. By reconnecting the button we tell it to include the 'sender' argument, which it otherwise wouldn't have done and that would have lead to a runtime error since we longer have a zero-argument action defined in our source code. Finish off by reconnecting the "Round Rect Button (Third)" object to the 'next2:' event.

Test run!

Yes, it's time for a test run again - how fun! Remember to save the MainWindow.xib in IB by pressing CMD-S and then go back to XCode to build and run (CMD-Return). When the simulator is brought to the front, don't do anything before bringing XCode's console window to the front. Do this by selecting the source code window in XCode and press CMD-R. Press the "Second" button to move to the second view controller and then the "Third" button. This produce something like the following in the XCode console window:

2009-04-28 20:26:35.553 WinNav[13158:20b] next: sender=0x526aa0, button=0x526aa0, button2=0x523630
2009-04-28 20:26:36.784 WinNav[13158:20b] next2: sender=0x523630, button=0x526aa0, button2=0x523630

Notice that the value of the 'sender' argument is different in the two lines? In the first line ('next:') the sender value is equal to the button value and in the second line ('next2:'), the sender value is equal to the button2 value. The values you see here are hexadecimal representations of the pointers for the objects involved in the actions. So how do we interpret this information? It's really pretty logical. In the first line ('next:') the action was sent by the 'button' object which is "Rouded Rect Button (Second)" since we connected it to the 'next' event above. In the second ('next2:') it's the "Rounded Rect Button (Third)".

Cool, this means that we can find out which IB created object that sent us an action. If we have connected that IB object to a pointer in our code we can even make intelligent decisions based on the sender of an action since we know exactly which object it was. This probably means that we could have a single IBAction method which handles both of our "next"-buttons. If it is the "(Second)"-button, we push viewController2 and if it is the "(Third)" we push viewController3, but that I leave as an exercise to the reader. But please wait until after the tutorial to test that so we can continue the discussion using a common source code :)

Adding a slider

WinNavAppDelegate.h

Add the following line to the near end of the file, just above the @end-marker.

- (IBAction)slider:(id)sender;

That line defines an IBAction with one argument, the sender of the action. Since we're just going to use one slider, you might wonder what we need the sender argument for. This is because we want to be able to read the value of slider, that is, how far to the left or right it has been slided. For that we need the pointer to the UISlider object and that is exactly what we get in the 'sender' argument.

WinNavAppDelegate.m

Add the following line to the near end of the file, just above the @end-marker.

- (IBAction)slider:(id)sender {
NSLog(@"slider: sender=%p value=%f", sender, ((UISlider *)sender).value);
}

Here is the implementaiton of the argument. The only thing it does is log some diagnostic information to the console by calling NSLog(). If you check the API docs for UISlider you'll see that it has a property called 'value' which contains the value of the slider between 0.0 and 1.0 (0 to 100%). The "((UISlider *)sender)" magic is standard Objective C syntax for converting an "untyped" pointer into a "typed" pointer. To be able to access the UISlider class' property 'value' we need to work with a UISlider object. The compiler doesn't know that "(id)sender" is a pointer to an UISlider object, so we have to tell it. That's what the magic is used for. Build the project (CMD-R) to save all files and see that there are no compilation errors.

MainWindow.xib

Now that we have prepared the source code for adding a slider, we should add the slider in IB as well. Go to IB and double-click on the "View" object under the "View Controller (First)" object. This should bring up the edit window for that view. Drag a slider from the "Inputs & Values" section in the Library window (CMD-L) anywhere onto the view. Check the MainWindow.xib window to verify that the object appeard under the "View" object - it should be called "Horizontal Slider".

Now we need to connect the slider to our IBAction. You can do this by CTRL-dragging from "Horizontal Slider" to "Win Nav App Delegate" and connect it to the 'slider:' event. Did you notice the colon which indicates that this event takes one argument (the sender)? However, ther are two other ways of doing this in IB. CTRL-click on "Horizontal Slider" to pop up a window showing all its outlets, etc. Scroll down to the end and you should see a row called "Values changed". At the far end of that row there is a small circle. CTRL-drag this circle to "Win Nav App Delegate" to achieve the same result. Did you notice how the pop up window was made semi transparent in order to "get out of the way"? The third way of doing this is to select "Horizontal Slider" and press CMD-2 to bring up the Connections tab of the Inspector window. Here you'll once again see the "Values changed" row with the circle at the right end and yes you guessed it, this circle can also be CTRL-dragged!

Test run, test run, test run!

Ok, ok, we'll do a test run again. In fact, this is the final test run since we're done! Remember to save the IB file (CMD-S) and then go to XCode to build and run (CMD-Return). Bring up the XCode console (CMD-R) once the simulator has appeared and then play a little with the slider and you'll see something like this in the console:

2009-04-28 20:57:46.238 WinNav[13253:20b] slider: sender=0x523620 value=0.500000
2009-04-28 20:57:46.472 WinNav[13253:20b] slider: sender=0x523620 value=0.489474
2009-04-28 20:57:46.504 WinNav[13253:20b] slider: sender=0x523620 value=0.478947
2009-04-28 20:57:46.521 WinNav[13253:20b] slider: sender=0x523620 value=0.468421
2009-04-28 20:57:46.555 WinNav[13253:20b] slider: sender=0x523620 value=0.457895
2009-04-28 20:57:46.622 WinNav[13253:20b] slider: sender=0x523620 value=0.447368
2009-04-28 20:57:46.731 WinNav[13253:20b] slider: sender=0x523620 value=0.447368

See how the values decreased? That's because I slided to the left - towards zero. Pretty amazing huh and it wasn't even that hard!

Monday, April 27, 2009

iPhone tutorial: Navigation Controller from Scratch

In this post, we're going to test if we really understood what we learned in the previous parts. Those parts focused on analyses, but this time we're going to focus on synthesing, that is creating something of instead of just understanding. We're going to create a navigation controller which handles three custom made view controllers and provide the means to navigate between them. The title says we're going to start from scratch, but we're actually going to start from XCode's "Window-Based Application" template ,which is pretty close to "from scratch".

Start up XCode, choose "File/New Project" from the menu, select "Window-Based Application" and name it "WinNav". After XCode has done it's magic, we have the following project structure.

WinNav
  Classes
    WinNavAppDelegate.[hm]
  Resources
    MainWindow.xib
    Info.plist

When we're using Interface Builder (IB) to create objects for us, we should usually start by defining Objective C pointers in the source code and then use IB to "connect" the objects to the pointers. When the application is launched, the IB file loading procedure will actually assign values to the pointers.

Classes/WinNavAppDelegate.h

As was stated initially, we're going use a navigation controller to navigate between three custom view controllers. Furthermore, we're going to add "next"-buttons on the two first view controllers which will take us to the next view controller. Therefore, we start by declaring a navigation controller, three view controllers and two buttons - pretty logical, huh? How do we do this? Simple, open WinNavAppDelegate.h in XCode and add the following lines after the "UIWindow *window" line that came with the template:

// DON'T ADD THE FOLLOWING LINE - JUST USED FOR REFERENCE!!!
UIWindow *window;

// navigation controller
UINavigationController *navigationController;

// view controllers
UIViewController *viewController;
UIViewController *viewController2;
UIViewController *viewController3;

// buttons
UIButton *button;
UIButton *button2;


Since we're going to manipulate all these objects from IB, we have to declare them as properties (to make them accessible to external classes) as well as declare them as IBOutlets so that IB will detect them, when it scans our class defintion files. We do this by adding the following block of code after the 'window' property that came with the template.

// DON'T ADD THE FOLLOWING LINE - JUST USED FOR REFERENCE!!!
@property (nonatomic, retain) IBOutlet UIWindow *window;

// navigation controller
@property (nonatomic, retain) IBOutlet UINavigationController *navigationController;

// view controllers
@property (nonatomic, retain) IBOutlet UIViewController *viewController;
@property (nonatomic, retain) IBOutlet UIViewController *viewController2;
@property (nonatomic, retain) IBOutlet UIViewController *viewController3;

// buttons
@property (nonatomic, retain) IBOutlet UIButton *button;
@property (nonatomic, retain) IBOutlet UIButton *button2;
- (IBAction)next;
- (IBAction)next2;


If you've read the previous posts all this should be pretty straight forward to you, but you might have forgotten what the strange IBAction stuff is all about. An action is a way to associate an UI-event (touching a button for example) with an action (a method in a class).

Classes/WinNavAppDelegate.m

We declared a bunch of properties in the h-file, so the first thing we should do is to add matching @synthesize-instructions to actually create the code required to implement the property specifications.

// DON'T ADD THE FOLLOWING LINE - JUST USED FOR REFERENCE!!!
@synthesize window;

// navigation controller
@synthesize navigationController;

// view controllers
@synthesize viewController;
@synthesize viewController2;
@synthesize viewController3;

// buttons
@synthesize button;
@synthesize button2;


Ok, now that the properties are implemented, we should implement the action methods we declared using the IBAction statements in the h-file. Add the following lines to the end of the m-file, just above the @end-statement.

- (IBAction)next {
[navigationController pushViewController:viewController2 animated:YES];
}

- (IBAction)next2 {
[navigationController pushViewController:viewController3 animated:YES];
}


The 'next' method will be associate with the "next"-button on the first view controller and the "next2"-button will be associated with the "next"-button on the second view controller. Both these methods do the same thing, they "push" a view controller onto the navigation controller, which automatically (thanks to the navigation controller) will take us to the new view (the one we push). The button on the first view takes us to the second view and the button on the second view takes us to the third. Moving "backwards" is automatically handled by the navigation controller thanks to an automatically added "back"-button in the top left corner.

To make all the stuff we have declared visible once we start the application, we need to add the navigation controller's view to our window. We do this by adding a 'addSubView' method call in the 'applicationDidFinishLaunching' that came with the template:

// navigation controller
[window addSubview:[navigationController view]];

// DON'T ADD THE FOLLOWING LINE - JUST USED FOR REFERENCE!!!
[window makeKeyAndVisible];


Resources/MainWindow.xib

Now that we've prepared our source code for what we want to do, it's time to make an IB file which creates all the objects for us, so double-click on Resources/MainWindow.xib to load the file into IB. Switch to hierarchical view mode in the MainWindow.xib window in IB, by pressing the middle icon just above the "View mode" text in the upper left corner of the window. This should present the contents of the file as:

File's Owner [UIApplication]
First Responder [UIResponder]
Win Nav App Delegate [WinNavAppDelegate]
Window [UIWindow]

To really see if we understand how to use IB, we'll start by deleting the last two objects by selecting them and press backspace.

Win Nav App Delegate

So how do we re-create the "Win Nav App Delegate" object? Choose Tools/Library from the IB menu or press CMD-L. Select "Cocoa Touch Plugin" from the Library window and then "Controllers". Scroll down to the Object-icon and drag it into the MainWindow.xib window. It should be automatically selected so press CMD-4 to bring up the Identity tab of the Inspector window. Change the class to WinNavAppDelegate (our class) and you should see how the list of outlets get populated in the "Class outlets" section; all our buttons and view controllers as well as the window and navigation controller should be present. You should also see our two actions 'next' and 'next2' in the "Class actions" section.

Connect the "Win Nav App Delegate" to the 'delegate' property of "File's Owner" by selecting "File's owner" and CTRL-dragging from it to "Win Nav App Delegate". A blue line should appear and when you release the mouse button a window called "Outlets" should appear. Select 'delegate' from it. CTRL-click "Win Nav App Delegate" and then "File's Owner" to verify that the 'delegate' outlet is correctly connected. 

Window

Let's re-create the "Window" object, by going to the "Windows, Views & Bars" branch in the Library window. Drag the Window icon into the MainWindow.xib window. Connect the Window to the 'window' property of "Win Nav App Delegate" by selecting "Win Nav App Delegate" and CTRL-dragging from it to "Window".

Navigation Controller

What's next? Well, we need a navigation controller so select the "Controllers" branch in the Library window again. Drag the "Navigation Controller" into the MainWindow.xib window. See the small arrow to the left of the newly added object? Press it to expand the hierarchy under the controller and you should see a "Navigation Bar" object and a "View Controller (Navigation Item)" object. Yep, there's another small arrow to the left of this object, so press that as well. Look, a "Navigation Item (Navigation Item)" object appeared. IB apparently did a lot of work for us when we drag and dropped the "Navigation Controller" icon. Even if this is a "from scratch" tutorial we're going to accept this help since we'll be adding two more view controllers by ourselves soon. Connect this to the 'navigationController' outlet of "Win Nav App Delegate" by CTRL-dragging from "Win Nav App Delegate" to "Navigation Controller" and select the 'navigationController' outlet.

Navigation Bar

Automatically created when we dragged the "Navigation Controller" icon from the Library Window. This is used "internally" by the "Navigation Controller" and shouldn't be manipulated.

View Controller (Navigation Item)

Automatically created when we added the "Navigation Controller from the Library. It's a simple UIViewController, but CTRL-click it to see that it's 'navigationItem' property is connected to the "Navigation Item (Navigation Item)" object. This object was placed "under" the "Navigation Bar" object thanks to some special handling in IB - navigation controllers are prepared for it.

Navigation Item (Navigation Item)

Also automatically created by IB. This object was placed "under" the "View Controller" also thanks to some special handling in IB. It is used to display the title and navigation buttons of the "View Controller". Double-click it to bring the "edit window" for it to the front. Double-click the text "Navigation Item" at the top and change it to "First". Notice how the text "(First)" replaced ("Navigation Item)" in the MainWindow.xib window, both for the "Navigation Item" and the "View Controller".

A first test run

Let's take a brief pause and save our IB file (CMD-S), return to XCode to build and run (CMD-Return) and wait for the iPhone simulator to start. You should see a white background with a blue bar at the top saying "First". That's the title of the view controller that was automatically added by IB.

View Controller (Second)

To create this object, bring up the Library window in IB (CMD-L) and drag a "View Controller" icon from the "Controllers" section to the end of the list of objects in the MainWindow.xib window and drop it there. We should create a similar hierarchy as for the automatically created view controller so drag the "Navigation Item" icon from the "Windows, Views & Bars" secion in the Library and drop it onto the newly added view controller. It should "swallow" it so that it appears "under" it instead of just "below" it. Double-click the newly added navigation item to bring up the edit window for it and then double-click the "Title" text and change it to "Second".

Connect the newly created view controller object, by CTRL-dragging from the "Win Nav App Delegate" object to the "View Controller (Second)" object and choose the 'viewController2' outlet from the menu that pops up.

View Controller (Third)

Repeat the steps from above, but name it "Third" instead. Easy, huh?

Adding views

View controllers without views are pretty boring, so we'll add a view to each of our controllers. There are two ways of doing this. Either you drag the "View" icon from the "Windows, Views & Bars" section of the Library onto the appropriate "View Controller" object in the MainWindow.xib window or you double-click the appropriate "View Controller" object in the MainWindow.xib window to bring up the edit window and then drag the "View" icon into the edit window (the big area that says "View). Go ahead and try both alternatives.

Adding buttons

The navigation controller automatically provides a means for moving "backwards" among its view controllers, just like a "Back" button in a web browser, but it does not provide a "Forward button". Therefore, we have to provide our own. We need one button on the first view controller so we can get to the second and one on the second so we can get to the third.

Double-click on the first view controller (the one IB automatically added for us under the navigation controller) to bring up its edit window. As an alternative, you could also double-click on the view controllers view that we added above. Now, drag the "Round Rect Button" icon from the "Inputs & Values" section of the Library window (CMD-L) and drop the icon anywhere in the view controllers edit window that we just brought up. Don't drop it on the blue navigation bar at the top, though. Double-click the newly added button and name it "Second".

Connect the newly created button by CTRL-dragging from "Win Nav App Delegate" to the button and select the 'button' outlet from the menu that pops up.

Repeat the steps for the second view controller but name the button "Third" and connect it to the 'button2' outlet instead.

Time for a test run again

Save the IB-file (CMD-S) and go to XCode to build and run (CMD-R). You should see a white screen with the title "First" in the blue navigation bar at the top and a button titled "Second". That's pretty cool, but hey nothing happens when we press the button! What kind of tutorial is this? 

Bringing the buttons to life

Expand the hierarchies for the first and second "View" objects in the MainWindow.xib window by pressing the small arrow that appeared to the left of them when we added the buttons to them. You should see that a "Rounded Rect Button (Second)" and "Rounded Rect Button (Third)" have been added to the object hierarchy.

Select the first button in the MainWindow.xib window in IB and CTRL-drag to "Win Nav App Delegate" button. A small window named "Events" should pop up displaying the two IBActions 'next' and 'next2' we defined in Classes/WinNavAppDelegate.h. Choose 'next'.

Repeat the steps for the second button, but choose 'next2' from the list of events instead.

Time for another test run!

Save the IB-file (CMD-S) and go to XCode to build and run (CMD-R). Try to press the "Second"-button to see what happens! Whoa! That's amazing! Look at those smooth movements! Now try pressing the "Third"-button and see how we switch to the third view controller - pretty amazing, right? But that's not all, try pressing the "Second"-button in the upper left corner of the screen - inside the navigation bar. Yeah, that's a "Back"-button which takes us to the previous view controller. So now you can navigate freely between the three view controllers. Not bad, huh? ;)