Sunday, April 12, 2009

Tutorial: Analysing XCode's "Utility Application" iPhone-template source code

In the last post, we looked at XCode's "Utility Application" template mainly in Interface Builder (IB), but this time we will take a better look at the actual source code in XCode. So either start a new "Utility Application" project named "Util1" or load the "Util1" project we created in the last post.

We did look at some source in the last post and noticed that MainView.[hm], MainViewController.[hm], FlipsideView.[hm] and FlipsideViewController.[hm] all are "almost empty" or at least very unexciting. We also looked at Util1AppDelegate.[hm] and RootViewController.h in the last post, so that more or less leaves RootViewController.m for this post.

Application Controllers/RootViewController.m

- (void)viewDidLoad {
[super viewDidLoad];
MainViewController *viewController = [[MainViewController alloc] initWithNibName:@"MainView" bundle:nil];
self.mainViewController = viewController;
[viewController release];
[self.view insertSubview:mainViewController.view belowSubview:infoButton];

RootViewController is a UIViewController which implements a method called 'viewDidLoad'. This methods gets called when a UIViewController object is created from a nib file, or actually after its 'view' is set. Here we override that method so that we also can get a chance to manipulate the newly created RootViewController object.

The first thing we do is to call the viewDidLoad method in our super class (UIViewController) to take advantage of the initialisations it does. After that we allocate an object of class MainViewController and initialise it from a nib file called 'MainView' which is our Resources/MainView.xib file. That newly allocated MainViewController object is temporarily stored in a local pointer called 'viewController' and permanently stored in this object's property 'mainViewController'. After that we release our reference to the MainViewController object using the temporary variable. This is done because the 'mainViewController' property was flagged with the 'retain' keyword in RootViewController.h.

Finally, we insert the view of the MainViewController object we just read from MainView.xib below the 'infoButton'. We insert it below so that the 'infoButton' still will be visible - otherwise it could be blocked by the main view. The reason for why you see no code which sets up the view of 'mainViewController' or the 'infoButton' is that all that is done automatically when the xib files are read. Ok, we actually did the work ourselves in Interface Builder (IB), so the magic part is that we don't have to write any Objective-C source code for it - instead we did some CTRL-dragging in IB.

Ok, so when we implement the 'viewDidLoad' method we get the chance to do additions or changes to the UIViewController object which was read from a xib file. The reason for why we didn't do everything in IB is that some things are impossible to do in IB and others are just simpler to do in source code.

After this, we have a RootViewController object in memory. Two of it's properties have been set; 'infoButton' was set in IB and 'mainViewController' was set in the 'viewDidLoad' method above. 'flipsideViewController' and 'flipsideNavigationBar' are still unset. We'll wait with those a while and instead take a look at our 'toggleView' action, which is implemented in the method 'toggleView'!

- (IBAction)toggleView {
This method is called when the info or Done button is pressed.
It flips the displayed view from the main view to the flipside view and vice-versa.

Ok, this nice comment pretty well explains what this method does.

if (flipsideViewController == nil) {
[self loadFlipsideViewController];

Here we check if the 'flipsideViewController' property is set and if it isn't we call the 'loadFlipsideViewController' method which hopefully sets the property for us.

UIView *mainView = mainViewController.view;
UIView *flipsideView = flipsideViewController.view;

We get the view properties from our the two UIViewControllers that are involved in the flipping action - "main view" and "flipside view" and store them in two local pointers.

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:1];
[UIView setAnimationTransition:([mainView superview] ? UIViewAnimationTransitionFlipFromRight : UIViewAnimationTransitionFlipFromLeft) forView:self.view cache:YES];

Help! What on Earth is this? Well, it's actually one of the coolest features of the iPhone API since it allows you to easily implement all those incredibly cool graphical effects you see when you play around with your iPhone - for example flipping a screen over so you can look at the "back" of the screen. In iPhone language these effects are called "animations" since they, instead of making something happen immediately, make something happen smoothly over time, that is, animating it...

Animations change something from an initial state to a final state. The initial state often is the current state so often it's enough to describe the final state. Since you might want to animate a lot of "things" at once you have to describe them at once so that one thing doesn't start animating (moving) before another. Therefore, you start an "animation block" by calling 'beginAnimations' in the UIView class. Each animation block also has a time over which the animation will happen, specified by calling 'setAnimationDuration' with a number of seconds as an argument (this is a float, so 0.1, 1, 1.5, etc. are all valid values.) You can also specify how you want to animate; that is what "graphical effect" we want to use. Here we either want to flip from the right or from the left depending if we're moving from the main view to the flipside view or vice versa.

The line specifying "from right" or "from left" might be worth explaining. How do we decide if we're flipping from the "main view" or the "flipside view"? We check if the "main view" has a superview! If it does, it's the main view that is visible and we're flipping from the right. How did it get a superview? Remember the 'viewDidLoad' method we implemented above? In that we "inserted" the "main view" into the "root view" and thus the "main view" got its superview.

if ([mainView superview] != nil) {
[flipsideViewController viewWillAppear:YES];
[mainViewController viewWillDisappear:YES];
[mainView removeFromSuperview];
[infoButton removeFromSuperview];
[self.view addSubview:flipsideView];
[self.view insertSubview:flipsideNavigationBar aboveSubview:flipsideView];
[mainViewController viewDidDisappear:YES];
[flipsideViewController viewDidAppear:YES];

} else {
[mainViewController viewWillAppear:YES];
[flipsideViewController viewWillDisappear:YES];
[flipsideView removeFromSuperview];
[flipsideNavigationBar removeFromSuperview];
[self.view addSubview:mainView];
[self.view insertSubview:infoButton aboveSubview:mainViewController.view];
[flipsideViewController viewDidDisappear:YES];
[mainViewController viewDidAppear:YES];

In this fat block, we continue to set up the "final state" of our animation. Here too, we care about if we're animating from the "main view" to the "flipside view" or vice versa. As you can see(!) the code is almost self-explanatory thanks to the rather well chosen names of all the methods we call and all the objects we work with. What we basically do is removing everything that is associated with the view we are "flipping out" and adding everything associated with the view we're "flipping in".

[UIView commitAnimations];

This ends our animation block and actually commits the animations we described for execution. Exactly when they will happen is outside our control, but you can trust the iPhone to do its best to make it look good ;)

Ok, we still haven't seen how the 'flipsideViewController' and 'flipsideNavigationBar' properties of our RootViewController object gets set so let's take a look at the 'loadFlipsideViewController' method which is said to solve that problem for us.

- (void)loadFlipsideViewController {

FlipsideViewController *viewController = [[FlipsideViewController alloc] initWithNibName:@"FlipsideView" bundle:nil];
self.flipsideViewController = viewController;
[viewController release];

This is pretty similar to what we saw for the "main view" in the 'viewDidLoad' method. Allocate the object and initialise it using a nib file, in this case a FlipsideViewController from Resources/FlipsideView.xib.

// Set up the navigation bar
UINavigationBar *aNavigationBar = [[UINavigationBar alloc] initWithFrame:CGRectMake(0.0, 0.0, 320.0, 44.0)];
aNavigationBar.barStyle = UIBarStyleBlackOpaque;
self.flipsideNavigationBar = aNavigationBar;
[aNavigationBar release];

Cool, here we're creating a UINavigationBar programmatically (in source code) and store a reference to it in the 'flipsideNavigationBar' property of our RootViewController.

UIBarButtonItem *buttonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(toggleView)];

Add the "Done"-button and make it call 'toggleView' when pressed.

UINavigationItem *navigationItem = [[UINavigationItem alloc] initWithTitle:@"Util1"];

Create the "title".

navigationItem.rightBarButtonItem = buttonItem;
[flipsideNavigationBar pushNavigationItem:navigationItem animated:NO];
[navigationItem release];
[buttonItem release];

And close the case!

That's pretty much it! It took an impressive amount of words to explain this little application, but hopefully it has helped your understanding of the Interface Builder, the iPhone API and how to "think" when creating iPhone applications. A fine tuned mix of source code and "pre-packed" objects created with Interface Builder is often the right way to go even though everything, of course, can done programmatically - completely in source code.

No comments:

Post a Comment