10.7. Pickers

Pickers are click wheels for the iPhone: large, spinning dials that can host any number of different options. Pickers are used in place of drop-down menus to provide a graphically rich selection interface for the user. The UIPickerView class was designed as a full-blown view class rather than a control, due to its sheer complexity and screen real estate. This allows you to attach pickers to other views or windows.

10.7.1. Creating a Picker

A UIPickerView class consumes a whopping 320x216 pixels on the screen, but can be vertically offset anywhere in the window. Like tables, the UIPickerView class uses a data source. Unlike tables, pickers do not make use of index paths, but reference each row by an NSInteger value. Pickers can have multiple dials, each referred to as a component.

A picker view uses a delegate as a data source, allowing the data source to exist in a separate class or view controller, as in the case of the tables in Chapter 3 and the preferences tables and section lists described earlier in this chapter.

The display region of a picker view is automatically set at a default 320x216 frame. If you try to initialize the picker with a customized frame size, it will be ignored. You can place the picker anywhere on the screen, but they are generally located at either the top or bottom:

UIPickerView *pickerView = [ [ UIPickerView alloc ]
    initWithFrame: CGRectMake(0.0, 280.0, 0.0, 0.0)];
pickerView.delegate = self;
pickerView.dataSource = self;

10.7.1.1. Picking picker properties

Many of the picker view's properties have been privatized in the SDK, giving the developer less control over it than internal classes make available. Pickers have variable options to toggle sounds, make multiple selections, and make aesthetic tweaks, but these interfaces are not available to an SDK developer. The only aesthetic option available is the selection window.

To show a translucent window across the current selection, set the picker's show⁠sSe⁠lec⁠tionIndicator property to YES:

pickerView.showsSelectionIndicator = YES;

10.7.1.2. Picker data source

After creating the picker view, you must code the data source to provide information about the construction of the picker. The following methods are required to build a data source. They are required components of the UIPickerViewDataSource protocol:



numberOfComponentsInPickerView

Defines the number of individual click wheels (columns) to be displayed with the picker view.



numberOfRowsInComponent

You may assign each dial in the picker a different number of possible values (rows). This method should return the total number of rows for the dial number specified.

In addition to this, the UIPickerViewDelegate protocol implements the following methods to obtain specific information about the picker's components.



titleForRow

Returns the actual dial value for a given row of a given dial (component). These are returned as NSString objects.



viewForRow

This method can override the default behavior of the picker to display any UIView class in a component dial.



widthForComponent

Returns the width for a given component (dial). If this method is not implemented, the picker will size-to-fit.



rowHeightForComponent

Returns the height for a given component (dial). If this method is not implemented, the picker will size-to-fit.

The prototypes and function for these methods will be illustrated in the example later in this section.

10.7.2. Displaying the Picker

Once you have created and configured the picker view and coded your data source methods, you're ready to attach the picker to your view controller:

[ self.view addSubview: pickerView ];

10.7.3. Reading the Picker

The most direct way to obtain the index of the selected column in the picker view is to use the view's selectedRowInComponent method:

int selectedRow = [ pickerView selectedRowInComponent: 0 ];

A delegate method also exists and is notified whenever the user selects a row in the picker view. Use this to alert an object so it can respond to a new row selection:

pickerView.delegate = myObject;

To receive a notification when a dial's value is changed, add the following delegate method, named didSelectRow, to your class:

- (void)pickerView:(UIPickerView *)pickerView
   didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    NSLog(@"Selected row %d from dial %d", row, component);

    /* Additional code to handle row selection */
}

10.7.4. NosePicker: Picking Your Nose

In this example, a UIPickerView containing different nose styles and sizes is presented to the user (Figure 10-6). You will first create a view controller, which then hosts the picker view as a subview. You'll be able to scroll a list of noses and choose one.

Figure 10-6. NosePicker example


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

Example 10-14. NosePicker application delegate prototypes (NosePickerAppDelegate.h)
#import <UIKit/UIKit.h>

@class NosePickerViewController;

@interface NosePickerAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    NosePickerViewController *viewController;
}

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

@end

                                          

Example 10-15. NosePicker application delegate (NosePickerAppDelegate.m)
#import "NosePickerAppDelegate.h"
#import "NosePickerViewController.h"

@implementation NosePickerAppDelegate

@synthesize window;
@synthesize viewController;


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

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

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


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


@end

                                          

Example 10-16. NosePicker view controller prototypes (NosePickerViewController.h)
#import <UIKit/UIKit.h>

@protocol UIPickerViewDataSource, UIPickerViewDelegate;

@interface NosePickerViewController : UIViewController <UIPickerViewDelegate,
 UIPickerViewDataSource> {
    UIPickerView *pickerView;
    UITextView *textView;
    NSMutableArray *noses;
    NSMutableArray *sizes;
    int selection[2];
}
- (void)updateText;
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView;
- (NSInteger)pickerView:(UIPickerView *)pickerView
    numberOfRowsInComponent:(NSInteger)component;
- (NSString *)pickerView:(UIPickerView *)pickerView
    titleForRow:(NSInteger)row forComponent:(NSInteger)component;
- (void)pickerView:(UIPickerView *)pickerView
    didSelectRow:(NSInteger)row inComponent:(NSInteger)component;

@end

                                          

Example 10-17. NosePicker view controller (NosePickerViewController.m)
#import "NosePickerViewController.h"

@implementation NosePickerViewController

- (id)init {
    self = [ super init ];
    if (self != nil) {
        noses = [ [ NSMutableArray alloc ] init ];
        [ noses addObject: @"Straight" ];
        [ noses addObject: @"Aquiline" ];
        [ noses addObject: @"Retrousse" ];
        [ noses addObject: @"Busque" ];
        [ noses addObject: @"Sinuous" ];
        [ noses addObject: @"Melanesian" ];
        [ noses addObject: @"African" ];

        sizes = [ [ NSMutableArray alloc ] init ];
        [ sizes addObject: @"Small" ];
        [ sizes addObject: @"Medium" ];
        [ sizes addObject: @"Large" ];
        [ sizes addObject: @"Super-Size" ];

        selection[0] = selection[1] = 0;
    }
    return self;
}

- (void)loadView {
    CGRect bounds = [ [ UIScreen mainScreen ] applicationFrame ];

    [ super loadView ];

    pickerView = [ [ UIPickerView alloc ]
        initWithFrame: CGRectMake(0.0, bounds.size.height - 216.0, 0.0, 0.0)
    ];

    pickerView.delegate = self;
    pickerView.dataSource = self;
    pickerView.showsSelectionIndicator = YES;
    [ self.view addSubview: pickerView ];

    textView = [ [ UITextView alloc ] initWithFrame:
        CGRectMake(0.0, 0.0, bounds.size.width, bounds.size.height - 216.0)
    ];

    textView.font = [ UIFont fontWithName: @"Arial" size: 18.0 ];
    textView.textAlignment = UITextAlignmentCenter;
    textView.editable = NO;

    [ self updateText ];
    [ self.view addSubview: textView ];
}

- (void)updateText {
    textView.text = [ NSString stringWithFormat:
        @"One %@, %@ schnoz:\nComing right up!\n",
        [ sizes objectAtIndex: selection[1] ],
        [ noses objectAtIndex: selection[0] ]
    ];
}

- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
    return 2;
}

- (NSInteger)pickerView:(UIPickerView *)pickerView
     numberOfRowsInComponent:(NSInteger)component
{
    switch(component) {
        case(0):
            return [ noses count ];
            break;
        case(1):
            return [ sizes count ];
            break;
    }
    return 0;
}

- (NSString *)pickerView:(UIPickerView *)pickerView
    titleForRow:(NSInteger)row forComponent:(NSInteger)component
{
    switch (component) {
        case(0):
            return [ noses objectAtIndex: row ];
            break;
        case(1):
            return [ sizes objectAtIndex: row ];
    }
    return nil;
}

- (void)pickerView:(UIPickerView *)pickerView
      didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
    selection[component] = row;
    [ self updateText ];
}

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


- (void)didReceiveMemoryWarning {
    [ super didReceiveMemoryWarning ];
}

- (void)dealloc {
    [ pickerView release ];
    [ noses release ];
    [ sizes release ];
    [ super dealloc ];
}

@end

                                          

Example 10-18. NosePicker main (main.m)
#import <UIKit/UIKit.h>

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

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

                                          

10.7.5. What's Going On

  1. When the application instantiates, a new view controller is created which, in turn, initializes an array of noses and nose sizes. Variable storage is also initialized for the currently selected dials, stored in selection.

  2. When the view controller's loadView method is invoked, UIPickerView and UITextView objects are created and added as subviews to the view controller. The delegate and data source for the picker view are set to the view controller. The controller's updateText method is invoked once to set the text within the text view.

  3. The UIPickerView class's internal plumbing calls its data source, which returns the number of components, rows, and titles for each row in the picker view.

  4. When the user selects a new item from either dial on the picker view, the view's delegate (the view controller) is notified through the picker view didSelectRow method. This updates the last row selected for a given dial and refreshes the text display to reflect the currently selected nose and size combination.

10.7.6. Further Study

When you've finished picking your nose, give some of these other exercises a try:

  • Check out the UIPickerView.h prototypes. You'll find these deep within /Developer/Platforms/iPhoneOS.platform, in the UI Kit framework's Headers directory.

  • Inside the prototypes, you'll find an alternative set of data source methods, which return UIView objects instead of NSString objects. Use these to create a set of custom UIImageView objects for the picker.