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!

4 comments:

  1. Awsome tutorial! Just like all your others. I am trying to resolve the exercise you gave at the middle of this tutorial but I havn't found the way yet. I'd be very thankful if you could resolve this riddle for me.
    I'm looking for a way to create such a method for several buttons, without creating if statements. Is there a way to use the 'sender' argument in such a way like:
    pushViewController: (sender+1)viewcontroller
    ?

    ReplyDelete
  2. @uriashi: Thanks, always nice to hear that someone enjoys the tutorials! Regarding the "riddle", I think you already have solved it. I don't remember exactly which answer I had in mind when I wrote the tutorial, but I think I just wanted to make the point that you don't need a separate method per button. So, deciding what action to perform using an if- or switch-statement is the correct answer!

    Regarding the second part of your comment, I am not sure that I understand what you try to accomplish with the "(sender+1)" statement. Do remember that the 'sender' argument is nothing but a pointer to an UIControl (or subclass) object. All you can do with this pointer is compare it to another pointer, or use it to get access to a member variable or method of the object.

    ReplyDelete
  3. What I'm trying to create is some sort of a "forward" button in a book, that will act like the built-in back button in a navigation controller - the method should "learn" from the sender id (a specific button from a specific viewcontroller) which is the next viewcontroller to push.

    ReplyDelete
  4. Sweet! I figured out the challenge in one shot. Except for one thing, i forgot to disconnect the buttons from next and next2, so it tried to push the viewControllers twice :P

    ReplyDelete