10.2. Preferences Tables

Preferences tables provide an aesthetically rich interface for displaying and changing program settings or displaying structured information, such as a contact or network info. When possible, an application should use a preference bundle (discussed in Chapter 11) to add a settings tab to the iPhone's Settings application, but the downfall to this is that it requires quitting your application to change settings. Apple has provided sanctioned APIs for creating preferences tables in your application too, which is useful for changing runtime settings. In addition to settings, if your application displays a grouped table of information, such as a proprietary display of contacts or network information, this type of table may also be useful to organize your data. Preferences tables provide resizable cells capable of hosting controls, text boxes, and informational text. They also provide a mechanism for logically grouping similar preferences together.

The SDK has conveniently wrapped the preferences table class into the existing UITableView class you learned about in Chapter 3. As a result, you can create a preferences table in much the same way as a standard table, with only minor tweaks to adjust its style and logical groupings.

NOTE

 

Deep down, the interfacing with the UITableView class instantiates a lower-level UI Kit object named UIPreferencesTable. This object is hidden from the SDK, but used widely by open source developers writing code with the third-party tool chain. The SDK has made it more convenient to work with all major table structures by consolidating the interfaces for them into the UITableView class, which is all you'll need to learn.

10.2.1. Creating a Preferences Table

You must put some forethought into implementing a preferences table, as a data source delegate is used to query for the information used to fill the table. You do this in a similar fashion to the generic list-like implementation of the UITableView class you learned about in Chapter 3, but with a higher level of complexity. The runtime class invokes a set of protocol methods in the data source to return information about the preferences table, just like a standard table. Much to the discouragement of the iPhone developer community, this is quite distant from an object-oriented model. Instead, the construction for the entire preferences table is bulky and complex, in contrast with Apple's traditionally elegant design style.

Just to recap, the preferences table refers to a complete settings page or an information page. A table can have many logical groupings of like settings. Within each group, a single table cell displays each individual setting to the user. The content for the cell includes optional title, text, and controls, if any.

The conversation between a preferences table and its data source looks (something) like Figure 10-1.

Because a preferences table is assembled in two pieces (the table and the data source), the cleanest way to put one together is to create a subclass of UITableViewController as you did in Chapter 3, and have it act as the table's data source. This allows your application to create an instance of the controller class (which includes its own table object) and display it on the screen.

10.2.1.1. Subclassing the table view controller

To create a self-contained preferences table, create a subclass of UITableViewController and include all of the protocol methods needed to bind it as a data source delegate. The following example creates a subclass named MyPreferencesViewController:

@interface MyPreferencesViewController : UITableViewController
{

}

/* Preferences table methods */

- (id)init;
- (void)loadView;
- (void)dealloc;

/* Data source methods */

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath;
- (CGFloat)tableView:(UITableView *)tableView
    heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section;

Figure 10-1. Analogy for dialog between a preferences table and its data source


The methods used for the data source break down as follows:



numberOfSectionsInTableView

Returns the number of logical groups in the preferences table. The group count should not include group labels. Each logical group will appear as a separate "balloon" on the screen.



numberOfRowsInSection

Returns the number of rows in the given preferences group. The row count should not include group labels. Your data source will treat each row as a UITableViewCell object, returned by the cellForRowAtIndexPath method, explained next.



cellForRowAtIndexPath

Returns a UITableViewCell object corresponding to the group and row specified. This method should check for an existing cell on the queue in the same way as the TableDemo table example from Chapter 3.



titleForHeaderInSection

Returns an NSString object containing the group label for the given preferences group.



heightForRowAtIndexPath

Returns a custom height for the group and row specified. This allows you to customize certain cells to be a specific height.

10.2.1.2. Initializing the table

When you initialize a table view controller, an internal UITableView object is created within the class to represent the table view belonging to the controller. Because you're creating a special type of table (a preferences table), you'll need to create a table view controller with a different underlying table style. Override your MyPreferencesViewCon⁠troller class's init method and manually create the table view using the UITableViewController superclass's alternative initWithStyle method:

(id)init {
    self = [ super initWithStyle: UITableViewStyleGrouped ];

    if (self != nil) {

        /* Additional initialization code */
    }

    return self;
}

If you are working with a UITableView by itself (that is, without a table view controller), you can use the table view class's alternative initWithFrame method, which includes a style argument. An example follows:

UITableView *myTableView = [ [ UITableView alloc ]
    initWithFrame: myRect
    style: UITableViewStyleGrouped
];

After creating the object, call its reloadData method to load all the elements in the preference table. This causes the table to invoke its data source and begin loading information about cell groupings and geometry. You can manually reload the table at any time by calling the table class's reloadData method:

[ self.tableView reloadData ];

10.2.1.3. Preferences table cells

Each cell in a preferences table is created as a UITableViewCell object, or a subclass of it. Cells are returned through the cellForRowAtIndexPath callback method, which you must write and which is called by the preferences table class automatically as new rows are being drawn on the screen. For example, if your table invokes the cellForRowA⁠tIn⁠dexPath method, specifying index path [ 0 1 ], the method is expected to return the cell corresponding to the first group (group 0) and the second row (row 1). The method should check the queue to ensure the cell has not already been created, and then create and return it:

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

    NSString *CellIdentifier = [ fileList objectAtIndex:
        [ indexPath indexAtPosition: 1 ]
    ];

    UITableViewCell *cell = [ tableView
        dequeueReusableCellWithIdentifier: CellIdentifier
    ];

    /* Create cell from scratch */
    if (cell == nil) {
        cell = [ [ [ UITableViewCell alloc ]
            initWithFrame: CGRectZero
            reuseIdentifier: CellIdentifier
          ] autorelease ];

        /* Example cell contents */
        cell.text = @"Debugging";
    }

    /* Return either the new cell or the queued cell */
    return cell;
}

10.2.1.4. Controls

The UITableViewCell class can accommodate a number of different controls, allowing you to add a switch, slider, or other type of control to an individual preferences table cell. Add controls to the UITableViewCell object as a subview.

When creating a new preferences table cell, add the control using the cell's addSubview method:

cell = [ [ [ UITableViewCell alloc ] initWithFrame: CGRectZero
            reuseIdentifier: CellIdentifier
          ] autorelease ];

cell.text = @"Advanced Mode";

/* Add a switch to the example cell */
UISwitch *debugSwitch = [ [ UISwitch alloc ]
    initWithFrame: CGRectMake(200.0, 10.0, 0.0, 0.0)
];

/* Attach the switch to the cell */
[ cell addSubview: debugSwitch ];

The preceding example creates a switch control with a frame offset to the right of the cell. The example also creates a cell with the title "Advanced Mode" and attaches the switch control to it. All of this takes place within the cellForRowAtIndexPath method, before returning the newly created cell.

10.2.1.5. Text fields

You can add text fields in the same way as other controls. By using the UIControl class's notification framework, edits and other relevant events can be relayed to your delegate class.

Text fields include an offset and a display region specifying the size of the text field. This allows you to create either a dedicated text cell (displaying only text across the entire cell), or a cell with both a boldface title (set with the title property) and a text field:

UITextField *textField = [ [ UITextField alloc ]
    initWithFrame: CGRectMake(20.0, 10.0, 280.0, 50.0)
];

 [ cell addSubview: textField ];

You can also display text and make it uneditable using the setEnabled method:

textField.text = @"Some text";
[ cell setEnabled: NO ];

You can read the value using the same property:

NSString *text = textField.text;

10.2.2. Displaying the Preferences Table

A preferences table is displayed in the same way as a view controller—by attaching its underlying view to a window or by pushing it onto a navigation controller.

Use the following to set the active view for the window:

[ window addSubview: myTableViewController.view ];

Use the following to push it onto a navigation controller:

[ navigationController pushViewController: myTableViewController.view
        animated: YES
    ];

10.2.3. ShootStuffUp: Preferences Table Example

You are writing a spaceship shoot-'em-up game, which needs a set of preferences to control everything from sound volume to debugging messages. In this example, the UITableViewController class is subclassed to create a custom ShootStuffUpTableViewController object (Figure 10-2). This object contains its own data source for the underlying table structure. It creates each cell and assigns some of the controls you learned about in the previous section.

Figure 10-2. ShootStuffUp example


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

Example 10-1. ShootStuffUp application delegate prototypes (ShootStuffUpAppDelegate.h)

 

#import <UIKit/UIKit.h>

@class ShootStuffUpViewController;

@interface ShootStuffUpAppDelegate : NSObject <UIApplicationDelegate> {
    UIWindow *window;
    ShootStuffUpViewController *viewController;
    UINavigationController *navigationController;
}

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

@end

                                          

Example 10-2. ShootStuffUp application delegate (ShootStuffUpAppDelegate.m)
#import "ShootStuffUpAppDelegate.h"
#import "ShootStuffUpViewController.h"

@implementation ShootStuffUpAppDelegate

@synthesize window;
@synthesize viewController;


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

    self.window = [ [ [ UIWindow alloc ] initWithFrame: screenBounds ] autorelease ];
    viewController = [ [ ShootStuffUpViewController alloc ] init ];
    navigationController = [ [ UINavigationController alloc ]
 initWithRootViewController: viewController ];

    [ window addSubview: [ navigationController view ] ];
    [ window makeKeyAndVisible ];
}


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


@end

                                          

Example 10-3. ShootStuffUp table view controller prototypes (ShootStuffUpViewController.h)
#import <UIKit/UIKit.h>

@interface ShootStuffUpViewController : UITableViewController {

    UISlider *musicVolumeControl;
    UISlider *gameVolumeControl;
    UISegmentedControl *difficultyControl;

    UISlider *shipStabilityControl;
    UISwitch *badGuyControl;
    UISwitch *debugControl;

    UITextField *versionControl;
}

- (id) init;
- (void) dealloc;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsIn
Section:(NSInteger)section;
- (NSString *)tableView:(UITableView *)tableView titleForHeader
InSection:(NSInteger)section;
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRow
AtIndexPath:(NSIndexPath *)indexPath;

@end

Example 10-4. ShootStuffUp table view controller (ShootStuffUpViewController.m)
#import "ShootStuffUpViewController.h"

@implementation ShootStuffUpViewController

- (id) init {
    self = [ super initWithStyle: UITableViewStyleGrouped ];

    if (self != nil) {
        self.title = @"Game Settings";
    }
    return self;
}

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

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

- (void)didReceiveMemoryWarning {

    [ super didReceiveMemoryWarning ];
}

- (void)dealloc {
    [ musicVolumeControl release ];
    [ gameVolumeControl release ];
    [ difficultyControl release ];
    [ shipStabilityControl release ];
    [ badGuyControl release ];
    [ debugControl release ];
    [ versionControl release ];
    [ super dealloc ];
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 3;
}

- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section
{
    switch (section) {
        case(0):
            return 3;
            break;
        case(1):
            return 3;
            break;
        case(2):
            return 1;
            break;
    }

    return 0;
}

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section
{
    switch (section) {
        case(0):
            return @"Game Settings";
            break;
        case(1):
            return @"Advanced Settings";
            break;
        case(2):
            return @"About";
            break;
    }
    return nil;
}

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSString *CellIdentifier = [ NSString stringWithFormat: @"%d:%d",
        [ indexPath indexAtPosition: 0 ], [ indexPath indexAtPosition:1 ]
    ];

    UITableViewCell *cell = [ tableView
        dequeueReusableCellWithIdentifier: CellIdentifier
    ];

    if (cell == nil) {
        cell = [ [ [ UITableViewCell alloc ]
            initWithFrame: CGRectZero reuseIdentifier: CellIdentifier
        ] autorelease ];

        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        switch ([ indexPath indexAtPosition: 0]) {
            case(0):
                switch([ indexPath indexAtPosition: 1]) {
                    case(0):
                        musicVolumeControl = [ [ UISlider alloc ]
                                 initWithFrame: CGRectMake(170, 0, 125, 50) ];
                        musicVolumeControl.minimumValue = 0.0;
                        musicVolumeControl.maximumValue = 10.0;
                        musicVolumeControl.value = 3.5;
                        [ cell addSubview: musicVolumeControl ];
                        cell.text = @"Music Volume";
                        break;
                    case(1):
                        gameVolumeControl = [ [ UISlider alloc ]
                                 initWithFrame: CGRectMake(170, 0, 125, 50) ];
                        gameVolumeControl.minimumValue = 0.0;
                        gameVolumeControl.maximumValue = 10.0;
                        gameVolumeControl.value = 3.5;
                        [ cell addSubview: gameVolumeControl ];
                        cell.text = @"Game Volume";
                        break;
                    case(2):
                        difficultyControl = [ [ UISegmentedControl alloc ]
                                 initWithFrame: CGRectMake(170, 5, 125, 35) ];
                        [ difficultyControl insertSegmentWithTitle: @"Easy"
                                 atIndex: 0 animated: NO ];
                        [ difficultyControl insertSegmentWithTitle: @"Hard"
                                 atIndex: 1 animated: NO ];
                         difficultyControl.selectedSegmentIndex = 0;
                        [ cell addSubview: difficultyControl ];
                        cell.text = @"Difficulty";
                        break;
                }
                break;
            case(1):
                switch ([ indexPath indexAtPosition: 1 ]) {
                    case(0):
                        shipStabilityControl = [ [ UISlider alloc ]
                                 initWithFrame: CGRectMake(170, 0, 125, 50) ];
                        shipStabilityControl.minimumValue = 0.0;
                        shipStabilityControl.maximumValue = 10.0;
                        shipStabilityControl.value = 3.5;
                        [ cell addSubview: shipStabilityControl ];
                        cell.text = @"Ship Stability";
                        break;
                    case(1):
                        badGuyControl = [ [ UISwitch alloc ]
                                 initWithFrame: CGRectMake(200, 10, 0, 0) ];
                        badGuyControl.on = YES;
                        [ cell addSubview: badGuyControl ];
                        cell.text = @"Bad Guys";
                        break;
                    case(2):
                        debugControl = [ [ UISwitch alloc ]
                                 initWithFrame: CGRectMake(200, 10, 0, 0) ];
                        debugControl.on = NO;
                        [ cell addSubview: debugControl ];
                        cell.text = @"Debug";
                        break;
                }
                break;
            case(2):
                versionControl = [ [ UITextField alloc ]
                       initWithFrame: CGRectMake(170, 10, 125, 38) ];
                versionControl.text = @"1.0.0 Rev. B";
                [ cell addSubview: versionControl ];
                [ versionControl setEnabled: NO ];
                cell.text = @"Version";
                break;
        }
    }

    return cell;
}

@end

                                          

Example 10-5. ShootStuffUp main (main.m)
#import <UIKit/UIKit.h>

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

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

                                          

10.2.4. What's Going On

You've just read through a full-blown application that displays a preferences table. Here's how it works:

  1. When you run the application, the delegate class creates a subclass of the UITableViewController. This class incorporates the data source for the preferences table and underlying variable storage for controls.

  2. The table view controller's init method is overridden and instructs its superclass to create it having a preferences table style, UITableViewStyleGrouped.

  3. The table view controller is added to the navigation controller's stack, and when it is displayed, its reloadData method is invoked. Because we haven't overridden reloadData, the parent UITableView class's version of the method is invoked. This begins the communication to the data source by calling the various data source methods. The preferences table talks to the data source to establish the basic construction and geometry of the table and its controls.

  4. The cellForRowAtIndex method first checks to see whether the cell already exists in the table's memory queue. If not, a new UITableViewCell object is created. The cell's title and controls are set based on the row and group number, and any controls are created and added as subviews of the preference cell.

10.2.5. Further Study

Now that you've had a taste of how preferences tables work, try some exercises to better acquaint yourself:

  • Incorporate your knowledge of the UIControl class's notification framework to intercept value changes as they occur in real time.

  • Use examples from Chapter 3 to create a main view for this application that displays all of the preference's values. Use a navigation control to allow the user to navigate between the preferences table and your display view.