3.3. View Controllers

In the previous example, you created a custom view class named MainView, which took on all of the functionality of a UIView class, and also controlled a UITextView class, capable of displaying text. Imagine now that your application has many different screens, and needs to change between them. Or perhaps your application needs to provide landscape mode rotations. In such cases, you would find yourself in a time-consuming situation to roll your own custom class to control how the UITextView was displayed, or how to transition different text views onto the screen. While you could roll your own, the iPhone SDK provides a view controller class designed to handle all of this work for you.

The UIViewController class provides the low-level functionality needed to create and display one or more views, just like the example MainView class controlled UITextView from the previous example. Apple's view controller also adds built-in support for orientation changes, low memory notifications, and smooth transitions, all of which you'll learn about in this chapter.

In addition to the functionality provided by the UIViewController class, many other controller-type classes exist to make your life as a developer easier. You'll learn about these throughout this book. These controller classes require that you also use the UIViewController class to contain your views, so now is a good time to start using them in your code.

3.3.1. Creating a View Controller

A view controller acts much like the MainView object you created earlier. You'll create a subclass of UIViewController, which will inherit the functionality of its parent class. A prototype for the view controller version of MainView might look like the following example:

#import <UIKit/UIKit.h>
#import <UIKit/UITextView.h>

@interface MainViewController : UIViewController {
    UITextView *textView;
}
- (id)init;
- (void)dealloc;
- (void)loadView;
@end

Just like MainView, the variables used in the view are stored within the controller class. In this example, a UITextView object is stored in the controller for later display.

When a view controller is initialized, a simple method named init is invoked by the object's creator, instead of the initWithFrame method you used with UIView. A view controller is designed to accommodate orientation changes, so you will need to reconfigure its underlying views to new screen boundaries for events like orientation changes and when the views are initially created. In portrait mode, the screen resolution will be 320x480, but in landscape mode, the resolution will be 480x320.

When a view controller is created, a call to a method named loadView is made. You'll be responsible for coding this method to build the underlying UIView class and to anchor this view to the controller.

By default, a UIViewController class creates a UIView object when it is initialized. This object is displayed as the controller's view by default, and can be accessed by addressing self.view. When you add your own custom view classes to a controller, you have two options: you can anchor your custom view classes to the existing UIView object or replace the view object entirely.

To anchor a custom view class to the existing view, create the custom view in your loadView method, and then use the addSubview method to add it to the view's display layer. This can be particularly useful if you're displaying multiple views together, such as a table and a picker view. In the example below, two text views are created and displayed vertically:

- (void)loadView {

    CGRect bounds = [ [ UIScreen mainScreen ] applicationFrame ];

    textView1 = [ [ UITextView alloc ] initWithFrame:
        CGRectMake(0, 0, bounds.size.width, bounds.size.height / 2)
    ];

    textView2 = [ [ UITextView alloc ] initWithFrame:
        CGRectMake(0, bounds.size.height / 2,
            bounds.size.width,
            bounds.size.height / 2)
    ];

    textView1.text = @"Hello, World!";
    textView2.text = @"Hello again!";
    [ self.view addSubview: textView1 ];
    [ self.view addSubview: textView2 ];
}

If you'll only be displaying a single view at a time, or if you have written a standalone custom view class that can display many objects on its own, you may wish to replace the default UIView object entirely. You can do this by assigning your own view object to self.view. An example of this follows:

- (void)loadView {

    [ super loadView ];
    CGRect bounds = [ [ UIScreen mainScreen ] applicationFrame ];

    textView = [ [ UITextView alloc ] initWithFrame: bounds ];
    textView.text = @"Hello, World! ";
    self.view = textView;
}

NOTE

 

Notice here that it is no longer necessary to set the screen origin to be zero-offset to the window. This is because a window can now be created using the entire screen's bounds, so that the application can control the status bar portion of the screen.

The loadView method is generally only called only once, unless the controller's underlying view has been flushed. The controller will flush a view if it's not presently being displayed and memory is short. When this happens, a method within the controller, named didReceiveMemoryWarning, is notified, allowing you to perform any cleanup of your own resources. If the view needs to be displayed again, loadView will be reinvoked.

3.3.2. Loading from Interface Builder

On top of creating your own objects, you can instantiate view controllers from Interface Builder templates. This allows you to store the UI properties for the view class in an Interface Builder template and fuse it with your custom view controller class. To create a new view controller object, include your .xib file by adding it to your project's Resources folder. You can then instantiate the class using the UIViewController class's initWithNibName method:

MainViewController *myViewController = [
    [ MainViewController alloc ]
    initWithNibName: @"MainViewController"
    bundle: nil
];

3.3.3. Orientation Changes

When an orientation change occurs, the view controller will check with your object to see if it should change to the specified orientation. The default method provided by Xcode disables all nonportrait orientation changes:

-(BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
    return (interfaceOrientation == UIDeviceOrientationPortrait);
}

To support all valid orientations, return a YES value. To support only specific orientations, compare the input argument with one or more of the following enumerated values:



UIDeviceOrientationUnknown

Catchall for errors or hardware failures



UIDeviceOrientationPortrait

Oriented upright vertically in portrait mode



UIDeviceOrientationPortraitUpsideDown

Oriented upside-down vertically in portrait mode



UIDeviceOrientationLandscapeLeft

Device is rotated counter-clockwise in landscape mode



UIDeviceOrientationLandscapeRight

Device is rotated clockwise in landscape mode



UIDeviceOrientationFaceUp

Device is laying flat, face up, such as on a table



UIDeviceOrientationFaceDown

Device is laying flat, face down, such as on a table

After an orientation change, a method named didRotateFromInterfaceOrientation is invoked from within the view controller. You can use this to make any application-specific adjustments to accommodate an orientation change:

-  (void)didRotateFromInterfaceOrientation:
         (UIInterfaceOrientation)fromInterfaceOrientation
{
    /* Add post-orientation change code here */
}

3.3.4. Disposing of a View Controller

Finally, when the view controller is disposed of, its dealloc method is called. This method cleans up any objects that are local to the controller, and then calls its superclass's dealloc method:

- (void)dealloc {

    [ super dealloc ];
}

Your version of the dealloc method will override the class's own version, which is why its superclass's dealloc method is subsequently called. Any objects you've allocated within your subclass should be released in the dealloc method, which will free them when your view controller is released:

- (void)dealloc {
    [ textView release ];
    [ super dealloc ];
}

3.3.5. ControllerDemo: Hello World, View Controller Style

This example builds on the previous one by implementing the proper use of a view controller to take the place of MainView. As you'll see, the view controller encapsulates the text view in much the same way as the MainView class did, but instantly expands the functionality of your application by allowing for full rotation (along with other things you'll learn about later). This example shows the correct use of a view controller's init, loadView, and dealloc methods to build an underlying view, as well as some example routines to accommodate orientation changes.

You can compile this application, shown in Examples Example 3-7 through Example 3-11, with the SDK by creating a view-based application project named ControllerDemo. Be sure to pull out the Interface Builder code if you'd like to see how these objects are created from scratch.

Example 3-7. ControllerDemo application delegate prototypes (ControllerDemoAppDelegate.h)
#import <UIKit/UIKit.h>

@class ControllerDemoViewController;

@interface ControllerDemoAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    ControllerDemoViewController *viewController;
}

@property (nonatomic, retain) IBOutlet UIWindow *window;
@property (nonatomic, retain) IBOutlet ControllerDemoViewController *viewController;

@end

                                          

Example 3-8. ControllerDemo application delegate (ControllerDemoAppDelegate.m)
#import "ControllerDemoAppDelegate.h"
#import "ControllerDemoViewController.h"

@implementation ControllerDemoAppDelegate

@synthesize window;
@synthesize viewController;

- (void)applicationDidFinishLaunching:(UIApplication *)application {
    CGRect screenBounds = [ [ UIScreen mainScreen ] bounds ];

    self.window = [ [ [ UIWindow alloc ] initWithFrame: screenBounds ]
        autorelease
    ];

    viewController = [ [ ControllerDemoViewController alloc ] init ];

    [ window addSubview:viewController.view ];
    [ window makeKeyAndVisible ];
}


- (void)dealloc {
    [viewController release];
    [window release];
    [super dealloc];
}


@end

Example 3-9. ControllerDemo view controller prototype (ControllerDemoViewController.h)
#import <UIKit/UIKit.h>
#import <UIKit/UITextView.h>

@interface ControllerDemoViewController : UIViewController {
    NSString *helloWorld, *woahDizzy;
    UITextView *textView;
}

@end

Example 3-10. ControllerDemo view controller (ControllerDemoViewController.m)
#import "ControllerDemoViewController.h"

@implementation ControllerDemoViewController

- (id)init {

    self = [ super init ];

    if (self != nil) {
        /* Illustrate allocating some objects, even if we don't need to */
        helloWorld = [ [ NSString alloc ] initWithString: @"Hello, World!" ];
        woahDizzy = [ [ NSString alloc ] initWithString: @"Woah, I'm Dizzy!" ];
    }

    return self;
}

- (void)loadView {

    [ super loadView ];

    textView = [ [ UITextView alloc ] initWithFrame:
        [ [ UIScreen mainScreen ] applicationFrame ]
    ];

    textView.text = helloWorld;
    self.view = textView;
}

-(BOOL)shouldAutorotateToInterfaceOrientation:
(UIInterfaceOrientation)interfaceOrientation
{
    return YES;
}

-  (void)didRotateFromInterfaceOrientation:
(UIInterfaceOrientation)fromInterfaceOrientation
{
    textView.text = woahDizzy;
}

- (void)viewDidLoad {
    [ super viewDidLoad ];

    /* Add custom post-load code here */
}

- (void)didReceiveMemoryWarning {
    [ super didReceiveMemoryWarning ];

    /* Add custom low-memory code here */
}

- (void)dealloc {
    /* Here, the objects we've allocated are released */

    [ helloWorld release ];
    [ woahDizzy release ];

    [ textView release ];
    [ super dealloc ];
}

@end

                                          

Example 3-11. ControllerDemo main (main.m)
#import <UIKit/UIKit.h>

int main(int argc, char *argv[]) {

    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
    int retVal = UIApplicationMain(argc, argv, nil, @"ControllerDemoAppDelegate");
    [pool release];
    return retVal;
}

                                          

3.3.6. What's Going On

The ControllerDemo example contains everything you've seen so far, but replaces the MainView class with a view controller named ControllerDemoViewController:

  1. The application instantiates by calling the program's main function, which causes the ControllerDemoAppDelegate class's applicationDidFinishLaunching method to be notified when the application has launched.

  2. A window is created using the entire bounds of the screen. This is needed in order for screen rotation to work correctly.

  3. A view controller is created and its init method is invoked. When the view is displayed, the controller's loadView method is notified, which creates a UITextView object and assigns it the application's viewable region using the UIScreen class's applicationFrame method. The text view is then assigned as the view controller's active view by means of setting self.view.

  4. The view controller's active view is added as a subview of the window and the window is instructed to display. This in turn displays the text view.

  5. When the device is rotated, the view controller's shouldAutorotateToIn⁠terfa⁠ceOr⁠ientation method is consulted to see if it should change orientation. The example returns YES, and the view controller's internal methods handle all of the work.

  6. When the screen has finished rotating, the view controller's didRotateFromInterfaceOrientation method is called, which changes the contents of the text view.

3.3.7. Further Study

Now that you have a skeleton for any view application, play around with it for a little while before proceeding:

  • Try changing the origins and size of the frame used by the view controller and window. What happens to the window and its child? How about when changing the display origin of textView?

  • Check out the following prototypes in your SDK's header files: UIWindow.h, UIView.h, and UIViewController.h. You'll find these deep within /Developer/Plat⁠forms/iPhoneOS.platform, inside the UI Kit framework's Headers directory.