Table View Programming Guide For Iphone Os: User Experience: Tables
Table View Programming Guide For Iphone Os: User Experience: Tables
2010-03-24
Apple Inc. 2010 Apple Inc. All rights reserved. No part of this publication may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, mechanical, electronic, photocopying, recording, or otherwise, without prior written permission of Apple Inc., with the following exceptions: Any person is hereby authorized to store documentation on a single computer for personal use only and to print copies of documentation for personal use provided that the documentation contains Apples copyright notice. The Apple logo is a trademark of Apple Inc. Use of the keyboard Apple logo (Option-Shift-K) for commercial purposes without the prior written consent of Apple may constitute trademark infringement and unfair competition in violation of federal and state laws. No licenses, express or implied, are granted with respect to any of the technology described in this document. Apple retains all intellectual property rights associated with the technology described in this document. This document is intended to assist application developers to develop applications only for Apple-labeled computers. Every effort has been made to ensure that the information in this document is accurate. Apple is not responsible for typographical errors. Apple Inc. 1 Infinite Loop Cupertino, CA 95014 408-996-1010 Apple, the Apple logo, Cocoa, iPod, Objective-C, and Xcode are trademarks of Apple Inc., registered in the United States and other countries. Cocoa Touch and iPhone are trademarks of Apple Inc. Simultaneously published in the United States and Canada.
Even though Apple has reviewed this document, APPLE MAKES NO WARRANTY OR REPRESENTATION, EITHER EXPRESS OR IMPLIED, WITH RESPECT TO THIS DOCUMENT, ITS QUALITY, ACCURACY, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. AS A RESULT, THIS DOCUMENT IS PROVIDED AS IS, AND YOU, THE READER, ARE ASSUMING THE ENTIRE RISK AS TO ITS QUALITY AND ACCURACY.
IN NO EVENT WILL APPLE BE LIABLE FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES RESULTING FROM ANY DEFECT OR INACCURACY IN THIS DOCUMENT, even if advised of the possibility of such damages. THE WARRANTY AND REMEDIES SET FORTH ABOVE ARE EXCLUSIVE AND IN LIEU OF ALL OTHERS, ORAL OR WRITTEN, EXPRESS OR IMPLIED. No Apple dealer, agent, or employee is authorized to make any modification, extension, or addition to this warranty. Some states do not allow the exclusion or limitation of implied warranties or liability for incidental or consequential damages, so the above limitation or exclusion may not apply to you. This warranty gives you specific legal rights, and you may also have other rights which vary from state to state.
Contents
Introduction
Chapter 1
Chapter 2
Chapter 3
Chapter 4
3
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CONTENTS
Populating the Table View With Data 40 Populating an Indexed List 41 Optional Table-View Configurations 45 Chapter 5
Chapter 6
Managing Selections 67
Selections in Table Views 67 Responding to Selections 67 Programmatically Selecting and Scrolling 70
Chapter 7
Chapter 8
4
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
Chapter 1
Chapter 3
Chapter 4
5
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
Listing 4-7 Listing 4-8 Listing 4-9 Listing 4-10 Listing 4-11 Listing 4-12 Listing 4-13 Chapter 5
Preparing the data for the indexed list 43 Providing section-index data to the table view 44 Populating the rows of an indexed list 44 Adding a title to the table view 45 Returning a header title for a specific section 45 Custom indentation of a row 46 Varying row height 46
Chapter 6
Managing Selections 67
Listing 6-1 Listing 6-2 Listing 6-3 Listing 6-4 Listing 6-5 Responding to a row selection 68 Setting a switch object as an accessory view and responding to its action message 68 Managing a selection listexclusive list 69 Managing a selection listinclusive list 69 Programmatically selecting a row 70
Chapter 7
6
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
Figure 7-2 Listing 7-1 Listing 7-2 Listing 7-3 Listing 7-4 Listing 7-5 Listing 7-6 Listing 7-7 Listing 7-8 Chapter 8
Deletion of section and row and insertion of row 78 View controller responding to setEditing:animated: 74 Customizing the editing style of rows 74 Updating the data-model array and deleting the row 74 Adding an Add button to the navigation bar 75 Responding to a tap on the Add button 75 Adding the new item to the data-model array 75 Batch insertion and deletion methods 76 Inserting and deleting a block of rows in a table view 77
7
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
8
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
INTRODUCTION
A table view presents a scrollable list of items that may be divided into sections. Table views are versatile user-interface objects frequently found in iPhone applications, particularly productivity applications. They have many purposes:
To let users navigate through hierarchically structured data To present an indexed list of items To display detail information and controls in visually distinct groupings To present a selectable list of options Table views of various kinds
Figure I-1
A table view has only one column and allows vertical scrolling only. It consists of rows in sections. Each section can have a header and a footer that displays text or an image. However, many table views have only one section with no visible header or footer. Programmatically, UIKit identifies rows and sections through their index number: Sections are numbered 0 through n-1 from the top of a table view to the bottom; rows are numbered 0 through n-1 within a section. A table view can have its own header and footer, distinct from any section; the table header appears before the first row of the first section, and the table footer appears after the last row of the last section. Programmatically, a table view is an instance of UITableView in one of two basic styles, plain or grouped. A plain-style table view is an unbroken list; a grouped table view has visually distinct sections. A table view has a data source and might have a delegate. These objects provide the data for populating the sections and rows of the table view and customize its appearance and behavior.
9
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
INTRODUCTION
Related chapters: Table View Styles and Accessory Views (page 13)
At a Glance
Table Views are Composed From Cells in a Particular Style
A table view draws its visible rows using cellsthat is, UITableViewCell objects. Cells are views that can display text, images, or other kinds of content. They can have background views for both normal and selected states. Cells can also have accessory views, which function as controls for selection or setting an option. The UIKit framework defines four standard cell styles, each with its own layout of the three default content elements: main label, detail label, and image. You may also create your own custom cells to acquire a distinctive style for your applications table views. Related Chapters: Table View Styles and Accessory Views (page 13), A Closer Look at Table-View Cells (page 47)
There Are Several Ways to Create a Table View But the Basic Pattern is the Same
You create an instance of UITableView in one of the two styles and assign it a data source. Immediately after its created, the table view asks its data source for the number of sections, the number of rows in each section, and the table-view cell to use to draw each row. The data source manages the application data used for populating the sections and rows of the table view. The easiest and recommended way to create a table view is to create a custom UITableViewController object. UITableViewController creates the table view and assigns itself as delegate and data source. If your application is largely based on table views, use the Navigation-based Application template when you create the application project; this template includes an initial custom UITableViewController class and a separate Interface Builder nib file for the table view. You can create a table view with the help of Interface Builder or entirely in code. You can use a custom UIViewController object or even a non-controller object to manage a table view. Related Chapters: Navigating a Data Hierarchy With Table Views (page 23), Creating and Configuring a Table View (page 33)
10
At a Glance
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
INTRODUCTION
of data or a leaf node" in the hierarchy. If the former, the application displays a new table view; if the later, the application displays detail about the selected item in a grouped-style table view or some other kind of view. In table views that list a series of options, tapping a row simply selects its associated option. No subsequent view of data is displayed. Related Chapters: Navigating a Data Hierarchy With Table Views (page 23), Managing Selections (page 67)
See Also
The information presented in this introduction and in Table View Styles and Accessory Views (page 13) summarizes prescriptive information on table views presented in "Table Views, Text Views, and Web Views in iPhone Human Interface Guidelines. It is recommended that you read this for a complete description of the styles and characteristics of table views, as well as their recommended uses. Before reading this book, you should read iPhone Application Programming Guide to understand the basic process for developing iPhone applications. You should also consider reading View Controller Programming Guide for iPhone OS for general information about view controllers and for more detailed information about navigation controllers, which are frequently used in conjunction with table views. You will find the following sample-code projects to be instructive models for your own table view implementations:
11
INTRODUCTION
12
See Also
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 1
Table views come in distinctive styles that are suitable for specific purposes. In addition, UIKit provides standard styles for the cells used to draw the rows of table views. It also gives you standard accessory views (that is, controls) that you can include in cells.
A variation of plain-style table views associates an index with sections for quick navigation; Figure 1-2 shows an example of this kind of table view, which is called an indexed list. The index runs down the right edge of the table view with entries in the index corresponding to section header titles. Touching an item in the index scrolls the table view to the associated section. For example, the section headings could be two-letter state
13
CHAPTER 1
abbreviations and the rows for a section could be the cities in that state; touching at a certain spot in the index displays the cities for the selected state. The rows in indexed section lists should not have disclosure indicators or detail disclosure buttons, because these interfere with the index. Figure 1-2 A table view configured as an section index
The simplest kind of table view is a selection (or radio) list (see Figure 1-3). A selection list is a plain-style table view that presents a menu of options that users can select. It can limit the selection to one row or allow multiple selections. A selection list marks a selected row with a checkmark (see Figure 1-3).
14
CHAPTER 1
Figure 1-3
A grouped table view also displays a list of information, but it groups related rows in visually distinct sections. As shown in Figure 1-4, each section has rounded corners and appears against a bluish-gray background. Each section may have text or an image for its header or footer to provide some context or summary for the section. A grouped table works especially well for displaying the most detailed information in a data hierarchy. It allows you to separate details into conceptual groups and provide contextual information to help users understand it quickly. For example, the information about a contact in the contacts list is grouped into phone information, email addresses, street addresses, and other sections.
15
CHAPTER 1
Figure 1-4
The headers and footers of sections in a grouped table view have relative locations and sizes as indicated in Figure 1-5. Figure 1-5 Header and footer of a section
Padding Header Table cell Footer Padding
16
CHAPTER 1
The cell style for the rows in Figure 1-7 left-aligns the main title and puts a gray subtitle right under it. It also permits an image in the default image location. This style is used in the iPod application and is associated with the UITableViewCellStyleSubtitle constant.
17
CHAPTER 1
Figure 1-7
The cell style for the rows in Figure 1-8 left-aligns the main title and puts the subtitle in blue text and right-aligns it on the right side of the row. Images are not permitted. This style is used in the Settings application, where the subtitle indicates the current setting for a preference. It is associated with the UITableViewCellStyleValue1 constant. Figure 1-8 Table row style with right-aligned subtitle
18
CHAPTER 1
The cell style for the rows in Figure 1-9 puts the main title in blue and right-aligns it at a point thats indented from the left side of the row. The subtitle is left-aligned at a short distance to the right of this point. This style does not allow images. It is used in the Contacts part of the Phone application and is associated with the UITableViewCellStyleValue2 constant. Figure 1-9 Table row style in Contacts format
Accessory Views
There are three standard kinds of accessory views (shown with their accessory-type constant): Standard accessory views Description Disclosure indicatorUITableViewCellAccessoryDisclosureIndicator. You use the disclosure indicator when selecting a cell results in the display of another table view reflecting the next level in the data-model hierarchy. Detail disclosure buttonUITableViewCellAccessoryDetailDisclosureButton. You use the detail disclosure button when selecting a cell results in a detail view of that item (which may or may not be a table view).
Check markUITableViewCellAccessoryCheckmark. You use a checkmark when a touch on a row results in the selection of that item. This kind of table view is known as a selection list, and it is analogous to a pop-up list. Selection lists can limit selections to one row, or they can allowed multiple rows with checkmarks.
Accessory Views
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
19
CHAPTER 1
Instead of the standard accessory-view types, you may specify a control object (for example, a switch) or a custom view as the accessory view.
20
Accessory Views
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 2
The programmatic interface for table views is expressed through several classes, two formal protocols, and a category added to a Foundation framework class. You have to deal with all of these API components when implementing a table view.
Table View
A table view itself is an instance of the UITableView class. This class declares methods that allow you to configure the appearance of the table viewfor example, specifying the default height of rows or providing a view used as the header for the table. Other methods give you access to the currently selected row as well as specific rows or cells. You can call other methods of UITableView to manage selections, scroll the table view, and insert or delete rows and sections.
UITableView inherits from UIScrollView, which defines scrolling behavior for views with content larger than the size of the window. UITableView redefines the scrolling behavior to allow vertical scrolling only.
Table View
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
21
CHAPTER 2
22
CHAPTER 3
A common use of table viewsand one to which theyre ideally suitedis to navigate hierarchies of data. A table view at a top level of the hierarchy lists categories of data at the most general level. Users select a row to drill down to the next level in the hierarchy. At the bottom of the hierarchy is a view (often a table view) that presents details about a specific item (for example, an address-book record) and may allow users to edit the item. This section explains how you can map the levels of the data-model hierarchy to a succession of table views and describes how you can use the facilities of the UIKit framework to help you implement such navigation-based applications.
23
CHAPTER 3
A model object may also have relationships with other model objects, and it is through these relationships that a data model acquires hierarchical depth by composing an object graph. Relationships are of two general kinds in terms of cardinality: to-one and to-many. To-one relationships define an objects relationship with another object (for example, a parent relationship). A to-many relationship on the other hand defines an objects relationship with multiple objects of the same kind. The to-many relationship is characterized by containment and can be programmatically represented by collections such as NSArray objects (or, simply, arrays). An array might contain other arrays, or it could contain multiple dictionaries, which are collections that identify their contained values through keys. Dictionaries, in turn, can contain one or more other collections, including arrays, sets, and even other dictionaries. By collections thus nesting other collections, your data model can acquire hierarchical depth.
24
CHAPTER 3
Figure 3-1
East Bay
"Name" = "Sylvan Trail Loop" "Location" = "Edgewood City Park (Redwood City)" "Distance" = 2 "Difficulty" = "Moderate" "Restrictions" = "No bicycles, pets, or horses" "Map" = pen_map6.png // other key/value pairs trail dictionary
North Bay
Peninsula
South Bay
regions array
For the purpose of illustration, the above diagram shows a sequence of three table views navigating three levels of a data hierarchy. You could easily redesign this application so that there are only two table views. The first table view could be a list (plain style) in which each region is a section of the table view whose rows name the trails for that region. The data model could reflect this arrangement as an array nesting multiple arrays. When the table view asks its data source for the content for a particular row, it passes an NSIndexPath object that the data source can use to locate, first, the inner array (section property) and then the object within that array (row property).
25
CHAPTER 3
26
CHAPTER 3
Figure 3-2
Navigational control
The UINavigationController class inherits from UIViewController, a base class that defines the common programmatic interface and behavior for controller objects that manage views in iPhone OS. Through inheritance from this base class, a view controller acquires an interface for general view management. Once it implements parts of this interface, a view controller can autorotate its view, respond to low-memory notifications, overlay modal views, respond to taps on the Edit button, and otherwise manage the view. A UINavigationController manages the navigation bar, including the items that are displayed in the bar for the view below it. A UIViewController object manages a view displayed below the navigation bar. For this view controller, you create a subclass of UIViewController or a subclass of a view-controller class that the UIKit framework provides for managing a particular type of view. For table views, this view-controller class is UITableViewController. For a navigation controller that displays a sequence of table views reflecting levels within a data hierarchy, you need to create a separate custom table-view controller for each table view. A navigation controller manages a navigation bar that traverses a sequence of table views by maintaing a stack of view controllers, one for each of the table views displayed (see Figure 3-3). It begins with whats known as the root view controller. When the user taps a row of the table view (often on a disclosure indicator or a detail disclosure button), the root view controller pushes the next view controller onto the stack; the new view controllers table view visually slides into place from the right and the navigation bar items are updated appropriately. When users tap the back button in the navigation bar, the current view controller is popped off the stack. As a consequence, the navigation controller displays the table view managed by the view controller now at the top of the stack.
27
CHAPTER 3
Figure 3-3
UINavigationController UINavigationBar
UIViewController
UITableView
The UIViewController includes methods that allow view controllers to access and set the navigation items (which are UINavigationItem objects) displayed in the navigation bar for the currently displayed table view. It also declares a title property through which you can set the title of the navigation bar for the current table view. Not all applications need to use the navigation controller and view controller architectures. If your application is displaying a single table view and not a sequence of them, you dont need to use navigation controllers unless you want to have an Edit button or other specialized buttons appear in the navigation bar. Also, if an application shows only one table view, it doesnt even need a custom view controller to manage it. For example, an application that uses a table view to present a list of selectable options can have an object other than a view controller assume the roles of data source and delegate for the table view.
Table-View Controllers
Although you could manage a table view within the navigation-controller architecture using a direct subclass of UIViewController, you save yourself a lot of work if instead you subclass UITableViewController. The UITableViewController class takes care of many of the details you would have to implement if you were to create a direct subclass of UIViewController to manage a table view. You create a table-view controller by allocating memory for it and Initialization it with the initWithStyle: method, passing in either UITableViewStylePlain or UITableViewStyleGrouped for the required type of table view. Once you create a table-view controller, it either creates its table view or loads it from a nib file. In either case, the behavior is slightly different:
If a nib file is specified, the UITableViewController object loads the UITableView object archived in the nib file. (The nib file is usually specified as attribute of the UITableViewController, which must be the Files Owner of the nib file.) The table views attributes, size, and autoresizing characteristics are usually set in the nib file. The data source and delegate of the table view become those objects defined in the nib file, if any. If there is no nib file, the UITableViewController object allocates and initializes an unconfigured UITableView object with the correct dimensions and autoresize mask. It sets itself as the data source and the delegate of the table view; it also does this if the nib file defines no data source or delegate.
28
CHAPTER 3
When the table view is about to appear for the first time, the table-view controller sends reloadData to the table view, which prompts it to request data from its data source. The data source tells the table view how many sections and rows-per-section it wants, then gives the table view the data to display in each row. This process is described in Creating and Configuring a Table View (page 33). The UITableViewController class also performs other common tasks. It clears selections when the table view is about to be displayed and flashes the scroll indicators when the table finishes displaying. In addition, it responds properly when users tap the Edit button by putting the table view into editing mode (or taking it out of editing mode if users tap Done). The class exposes one property, tableView, which gives you access to the managed table view. Note: UITableViewController has new capabilities in iPhone OS 3.0. A table-view controller supports inline editing of table-view rows; if, for example, rows have embedded text fields in editing mode, it scrolls the row being edited above the virtual keyboard that is displayed. In addition, it now supports the NSFetchedResultsController class for managing the results returned from a Core Data fetch request. The UITableViewController class implements the foregoing behavior by overriding loadView, viewWillAppear:, and other methods inherited from UIViewController. In your subclass of UITableViewController, you may also override these methods to acquire specialized behavior. If you do override these methods, be sure to invoke the superclass implementation of the method, usually as the first method call, to get the default behavior. Note: You should use a UIViewController subclass rather than a subclass of UITableViewController to manage a table view if the view to be managed is composed of multiple subviews, one of which is a table view. The default behavior of the UITableViewController class is to make the table view fill the screen between the navigation bar and the tab bar (if either are present). If you decide to use a UIViewController subclass rather than a subclass of UITableViewController to manage a table view, you should perform a couple of the tasks mentioned above to conform to the human-interface guidelines. To clear any selection in the table view before its displayed, implement the viewWillAppear: method to clear the selected row (if any) by calling deselectRowAtIndexPath:animated:. After the table view has been displayed, you should flash the scroll views scroll indicators by sending a flashScrollIndicators message to the table view; you can do this in an override of the viewDidAppear: method of UIViewController.
29
CHAPTER 3
Note: This section summarizes view-controller and navigation-controller tasks with a focus on table views. For a thorough discussion of view controllers and navigation controllers, including the complete details of their implementation, see View Controller Programming Guide for iPhone OS. The sequence typically starts with the application delegate in its implementation of the applicationDidFinishLaunching: (or application:didFinishLaunchingWithOptions:) method. The delegate creates an instance of the UITableViewController subclass that is to be the root view controller. Then it allocates an instance of UINavigationController and initializes it with the just-created table-view controller with the initWithRootViewController: method. This initializer has the side effect of making the root view controller the first object on the stack of view controllers managed by the navigation controller After creating the navigation controller, the delegate adds the navigation controllers view to the window and makes the window visible. Listing 3-1 illustrates this series of calls. Listing 3-1 Setting up the root view controllerwindow in nib file
- (void)applicationDidFinishLaunching:(UIApplication *)application { RootViewController *rootViewController = [[RootViewController alloc] initWithStyle:UITableViewStylePlain]; NSArray *timeZones = [NSTimeZone knownTimeZoneNames]; rootViewController.timeZoneNames = [timeZones sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; UINavigationController *aNavigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; self.navigationController = aNavigationController; [aNavigationController release]; [rootViewController release]; [window addSubview:[navigationController view]]; [window makeKeyAndVisible]; }
The window in this example is unarchived from a nib file and assigned to an outlet of the application delegate. If you were to create the window programmatically, the code would look similar to Listing 3-2. (In this case, window is a property of the application delegate.) Listing 3-2 Setting up the root view controllerwindow created programmatically
- (void)applicationDidFinishLaunching:(UIApplication *)application { // Create the window window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; // Create the navigation and view controllers RootViewController *rootViewController = [[RootViewController alloc] initWithStyle:UITableViewStylePlain]; navigationController = [[UINavigationController alloc] initWithRootViewController:rootViewController]; [rootViewController release]; // Configure and show the window [window addSubview:[navigationController view]]; [window makeKeyAndVisible]; }
30
CHAPTER 3
When you initialize a table-view controller with initWithStyle:, that Initialization is invoked as well as the initializers inherited from superclasses:
If your table view is defined in a nib file, instead of setting the Nib Name attribute of the table-view controller (as Files Owner) in Interface Builder, you can override this method to call super, supplying the superclass with the name of the nib file.
You can override any of these initializers to perform set-up tasks common to view controllersfor example, setting the title of the navigation bar, as shown in Listing 3-3 (page 31). Listing 3-3 Setting the title of the navigation bar in init
- init { if (self = [super init]) { self.title = NSLocalizedString(@"List", @"List title"); } return self; }
You can perform the same set-up tasks in the loadView and viewDidLoad methods, which are invoked right after initialization. You can also in these methods set various UINavigationItem properties through the inherited navigationItem property. These properties generally are button items. Listing 3-4 shows how one table-view controller class places an Edit|Done and an add (+) button on its navigation bar. Listing 3-4 Setting the buttons of a navigation bar in loadView
- (void)loadView { [super viewDidLoad]; self.tableView.allowsSelectionDuringEditing = YES; self.navigationItem.leftBarButtonItem = self.editButtonItem; self.addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addItem:)]; self.navigationItem.rightBarButtonItem = self.addButton; }
At this point, lets assume the table view managed by our root table-view controller is presented to users. It is a list (or regular-style) table view. How does the application display the next table view in the sequence? When a user taps a row of the table view, the table view calls the tableView:didSelectRowAtIndexPath: or tableView:accessoryButtonTappedForRowWithIndexPath: method implemented by the its delegate. (That latter method is invoked if the user taps a rows detail-disclosure indicator.) The delegate in the example shown in Listing 3-5 creates the table-view controller managing the next table view in the sequence, sets the data it needs to populate its table view, and pushes this new view controller onto the navigation controllers stack of view controllers. Listing 3-5 Creating and pushing the next table-view controller on the stack
31
CHAPTER 3
This last code example is provided to give you a general idea of how a navigation-based application displays a sequence of table views. For complete details about handling selections in table views, see Managing Selections (page 67).
A view controller object (typically a UITableViewController object), acting in the role of data source, populates its table view with data from an object representing a level of the data hierarchy. The object is typically an array when the table view displays a list of items; when the table view displays item detail (that is, a leaf node of the data hierarchy), the object can be a custom model object, a Core Data managed object, a dictionary, or something similar.
The view controller stores the data it needs for populating its table view. The view controller can use this data directly for populating the table view, or it can use it to fetch or otherwise obtain the necessary data. When you design your view-controller subclass, you should define an instance variable (whose access is mediated via a declared property or accessor methods) to hold this data. View controllers should not obtain the data for their table view through a global variable or a singleton object such as the application delegate. Such direct dependencies make your code less reusable and more difficult to test and debug.
The current view controller on top of the navigation-controller stack creates the next view controller in the sequence and, before it pushes it onto the stack, sets the data this view controller, acting as data source, needs to populate its table view.
32
CHAPTER 4
Your application must present a table view to users before it can manage it in response to taps on rows and other actions. This chapter shows what you must do to create a table view, configure it, and populate it with data. Most of the code examples shown in this chapter come from the example projects TableViewSuite and TheElements.
3.
4.
5.
33
CHAPTER 4
Figure 4-1
Client
Data Source
numberOfSectionsInTableView: tableView:numberOfRowsInSection: tableView: cellForRowAtIndexPath:
The diagram in Figure 4-1 shows the required protocol methods as well as the numberOfSectionsInTableView: method. (Note that zero is a valid value to return for number of sections.) Populating the table view with data occurs in steps 3 through 5; Populating the Table View With Data (page 40) describes how you implement the methods mentioned in these steps. The data source and the delegate may implement other optional methods of their protocols to further configure the table view. For example, the data source might want to provide titles for each of the sections in the table view by implementing tableView:titleForHeaderInSection:. Optional Table-View Configurations (page 45) describes some of these optional table-view customizations. You create a table view in either the plain style (UITableViewStylePlain) or the grouped style (UITableViewStyleGrouped). (You specify the style when you initialize the table view by calling, directly or indirectly, the initWithFrame:style: method. Although, the procedure for creating a table view in either styles is identical, you may want to perform different kinds of configurations. For example, because a grouped table view generally presents item detail, you may also want to add custom accessory views (for example, switches and sliders) or custom content (for example, text fields) to cells in the delegates tableView:cellForRowAtIndexPath: method; A Closer Look at Table-View Cells (page 47) gives an example of this.
34
CHAPTER 4
Most applications use a custom UITableViewController object to manage a table view. As described in Navigating a Data Hierarchy With Table Views (page 23), UITableViewController automatically creates a table view, assigns itself as both delegate and data source (and adopts the corresponding protocols), and initiates the procedure for populating the table view with data. It also takes care of several other housekeeping details of behavior. The behavior of UITableViewController (a subclass of UIViewController) within the navigation-controller architecture is described in Table-View Controllers (page 28).
If your application is largely based on table views (a list application), select the Navigation-based Application template defined by Xcode when you create your project. As described in Creating a Table View Application the Easy Way (page 35), the template includes stub code and nib files defining an application delegate, the navigation controller, and the root view controller (which is an instance of a custom subclass of UITableViewController).
For successive table views, you should implement custom UITableViewController objects. You can either create the associated table views programmatically or you can load them from separate nib files. Although either option is possible, you may find the purely programmatic route easier. These options are discussed in Adding Table Views to the Application (page 38).
If the view to be managed is a composite view in which a table view is one of multiple subviews, you must use a custom subclass of UIViewController to manage the table view (and other views). Do not use a UITableViewController object because this controller class sizes the table view to fill the screen between the navigation bar and the tab bar (if either are present).
Xcode creates the table view in a nib file and creates an instance of a custom subclass of UITableViewController that, at runtime, loads the table view from the nib file, populates it, and manages it.
35
CHAPTER 4
Figure 4-2
Double-click MainWindow.xib to open this nib file in Interface Builder. In the document window for this nib file you can see the objects it contains (shown in Figure 4-3). The main top-level objects are File's Owner (a proxy for the application object itself ), the application delegate, the applications window, and a UINavigationController object. The last object is the root of an object graph that is significant for the table view. The direct children of the navigation controller are the navigation bar that runs across the screen above the table view and a placeholder for an instance of the RootViewController class of the project. Because it is a child of the navigation controller, this view controller is made the root view controller, the first one on the stack of view controllers managed by the navigation controller (explained in Navigating a Data Hierarchy With Table Views (page 23)). Figure 4-3 The contents of the main windows nib file
When the application is launched, the MainWindow.xib nib file is loaded into memory. The application delegate displays the initial user interface in the two lines of code in Listing 4-1. By asking the navigation controller for its view, the application delegate obtains the navigation bar, the title in the navigation bar (the navigation item), and the table view associated with the root view controller.
36
CHAPTER 4
Listing 4-1
But (you may have noticed) the Root View Controller object in the nib document window in Figure 4-3 (page 36) does not show a subordinate table view. Where does the table view come from? Select the Root View Controller object in the document window and display the attributes inspector for that object (Command-1). Here you see that the Nib Name field is set to RootViewController (as shown in Figure 4-4). When the delegate asks the navigation controller for its views, the navigation controller asks its root view controller for its view, and the root view controller loads the nib file specified through this attribute. Figure 4-4 Setting the nib-name property of the table-view controller
The RootViewController.xib nib file contains the table view and sets the RootViewController object to be Files Owner. To see the connections between RootViewController and its table view, right-click (or Control-click) Files Owner. This action displays a heads-up display as depicted in Figure 4-5. Figure 4-5 Connections in the root view controllers nib file
The RootViewController object keeps two references to the table view (one is via the view property inherited from UIViewController); it is also set to be the data source and delegate of the table view.
37
CHAPTER 4
You can also change the characteristics of the table view by selecting it in the nib document window and then going to the Attributes pane of the Interface Builder inspector. For example, you could change the style of the table view from plain (the default) to grouped. The table view managed by RootViewController is empty at runtime until the the table-view controller populates it with data; see Populating the Table View With Data (page 40) to learn the procedure for doing this. You can also programmatically configure the table view or the navigation bar, such as specifying the title of the navigation bar or the title of the table view. (Common sites for this configuration code are the initWithStyle: or viewDidLoad methods.) Optional Table-View Configurations (page 45) discusses some of the things you can do to customize the appearance or behavior of a table view.
As with the RootViewController class that comes with the Navigation-based Application template, you must implement the methods of the custom table-view controller class that populate the table view with data. You may also programmatically set properties of the table view or the navigation bar (typically in the initWithStyle: or viewDidLoad methods). When users select an item in the table view managed by the
38
CHAPTER 4
RootViewController class, you allocate and initialize an instance of the second table-view controller and
push it onto the stack managed by the applications navigation controller; Managing Selections (page 67) describes this procedure in detail. Note: Populating a table view with data and configuring a table view are discussed in Populating the Table View With Data (page 40) and Optional Table-View Configurations (page 45), respectively. If you prefer to load the table view managed by a custom table-view controller from a nib file, you must do the following: 1. 2. 3. 4. 5. 6. 7. In Interface Builder, create an empty Cocoa Touch nib file (File > New). Drag a UITableViewController object from the Interface Builder Library into the nib document window. Save the nib file in your project directory under an appropriate name and, when prompted, select your project to have the nib file added to it. Select Table View Controller in the nib document window and open the Identity pane of the inspector. Set the class to your custom table-view controller class. Select Files Owner in the nib document window and set its class identity to the custom table-view controller class. Customize the table view in Interface Builder. Select the table-view controller in the nib document window, open the Attributes pane of the inspector, and enter (or select) the name of the nib file in the Nib Name field.
When at runtime the current table-view controller allocates and initializes an instance of the new table-view controller, the associated nib file is loaded.
39
CHAPTER 4
The next step is for the client to allocate and initialize an instance of the UITableView class. Creating a Table View Application the Easy Way gives an example of a client that creates a UITableView object in the plain style, specifies its autoresizing characteristics, and then sets itself to be both data source and delegate. Again, keep in mind that the UITableViewController does all of this for you automatically. Listing 4-3 Creating a table view
- (void)loadView { UITableView *tableView = [[UITableView alloc] initWithFrame:[[UIScreen mainScreen] applicationFrame] style:UITableViewStylePlain]; tableView.autoresizingMask = UIViewAutoresizingFlexibleHeight|UIViewAutoresizingFlexibleWidth; tableView.delegate = self; tableView.dataSource = self; [tableView reloadData]; self.view = tableView; [tableView release]; }
Because in this example the class creating the table view is a subclass of UIViewController, it assigns the created table view to its view property, which it inherits from that class. It also sends a reloadData message to the table view, causing the table view to initiate the procedure for populating its sections and rows with data.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [regions count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { // Number of rows is the number of time zones in the region for the specified section. Region *region = [regions objectAtIndex:section]; return [region.timeZoneWrappers count]; }
40
CHAPTER 4
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { // The header for the section is the region name -- get this from the region at the section index. Region *region = [regions objectAtIndex:section]; return [region name]; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *MyIdentifier = @"MyIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier] autorelease]; } Region *region = [regions objectAtIndex:indexPath.section]; TimeZoneWrapper *timeZoneWrapper = [region.timeZoneWrappers objectAtIndex:indexPath.row]; cell.textLabel.text = timeZoneWrapper.localeName; return cell; }
The data source in its implementation of the tableView:cellForRowAtIndexPath: method returns a configured cell object that the table view can use to draw a row. For performance reasons, the data source tries to reuse cells as much as possible. It first asks the table view for a specific reusable cell object by sending it dequeueReusableCellWithIdentifier:. message. if no such object exists, the data source creates it, assigning it a reuse identifier. It sets the cells content (its text in this example) and returns it. A Closer Look at Table-View Cells (page 47) discusses this data-source method and UITableViewCell objects in more detail. The implementation of tableView:cellForRowAtIndexPath: in Listing 4-4 illustrates an aspect of the table view API that youll frequently encounter. The method includes an NSIndexPath argument that identifies the table-view section and row for the cell that the data source is to provide. (In this case, the section index is not needed because the table view has only one section.) UIKit declares a category of the NSIndexPath class, which is defined in the Foundation framework. This category extends the class to enable the identification of table-view rows by section and row index numbers. For information on this category, see NSIndexPath UIKit Additions.
41
CHAPTER 4
The data that that you use to populate an indexed list should be organized to reflect this indexing model. Specifically, you need to build an array of arrays. Each inner array corresponds to a section in the table; section arrays are sorted (or collated) within the outer array according to the prevailing ordering scheme, which is often an alphabetical scheme (for example, A through Z). Additionally, the items in each section array are sorted. You could build and sort this array of arrays yourself, but fortunately the UILocalizedIndexedCollation class makes the tasks of building and sorting these data structures and providing data to the table view much easier. The class also collates items in the arrays according to the current localization. Note: The UILocalizedIndexedCollation class was added to the UIKit framework in iPhone OS 3.0. However you internally manage this array-of-arrays structure is up to you. The objects to be collated should have a property or method that returns a string value that the UILocalizedIndexedCollation class uses in collation; if it is a method, it should have no parameters. You might find it convenient to define a custom model class whose instances represent the rows in the table view. These model objects not only return a string value but define a property that holds the index of the section array to which the object is assigned. Listing 4-5 illustrates the definition of a class that declares a name property for the former purpose and a sectionNumber property for the latter purpose. Listing 4-5 Defining the model-object interface
@interface State : NSObject { NSString *name; NSString *capitol; NSString *population; NSInteger sectionNumber; } @property(nonatomic,copy) NSString *name; @property(nonatomic,copy) NSString *capitol; @property(nonatomic,copy) NSString *population; @property NSInteger sectionNumber; @end
Before your table-view controller is asked to populate the table view, load the data to be used from whatever source and create instances of your model class from this data. The example in Listing 4-6 loads data defined in a property list and creates the model objects from that. It also obtains the shared instance of UILocalizedIndexedCollation and initializes the mutable array (states) that will contain the section arrays. Listing 4-6 Loading the table-view data and initializing the model objects
- (void)viewDidLoad { [super viewDidLoad]; UILocalizedIndexedCollation *theCollation = [UILocalizedIndexedCollation currentCollation]; self.states = [NSMutableArray arrayWithCapacity:1]; NSString *thePath = [[NSBundle mainBundle] pathForResource:@"States" ofType:@"plist"]; NSArray *tempArray; NSMutableArray *statesTemp; if (thePath && (tempArray = [NSArray arrayWithContentsOfFile:thePath]) ) { statesTemp = [NSMutableArray arrayWithCapacity:1]; for (NSDictionary *stateDict in tempArray) {
42
CHAPTER 4
State *aState = [[State alloc] init]; aState.name = [stateDict objectForKey:@"Name"]; aState.population = [stateDict objectForKey:@"Population"]; aState.capitol = [stateDict objectForKey:@"Capitol"]; [statesTemp addObject:aState]; [aState release]; } } else { return; }
Once the data source has this raw array of model objects, it can process it with the facilities of the UILocalizedIndexedCollation class. The code snippet in Listing 4-7 marks the significant parts with numbers. Listing 4-7 Preparing the data for the indexed list
// viewDidLoad continued... // (1) for (State *theState in statesTemp) { NSInteger sect = [theCollation sectionForObject:theState collationStringSelector:@selector(name)]; theState.sectionNumber = sect; } // (2) NSInteger highSection = [[theCollation sectionTitles] count]; NSMutableArray *sectionArrays = [NSMutableArray arrayWithCapacity:highSection]; for (int i=0; i<=highSection; i++) { NSMutableArray *sectionArray = [NSMutableArray arrayWithCapacity:1]; [sectionArrays addObject:sectionArray]; } // (3) for (State *theState in statesTemp) { [(NSMutableArray *)[sectionArrays objectAtIndex:theState.sectionNumber] addObject:theState]; } // (4) for (NSMutableArray *sectionArray in sectionArrays) { NSArray *sortedSection = [theCollation sortedArrayFromArray:sectionArray collationStringSelector:@selector(name)]; [self.states addObject:sortedSection]; } } // end of viewDidLoad
1.
The data source enumerates the array of model objects and sends sectionForObject:collationStringSelector: to the collation manager on each iteration. This method takes as arguments a model object and a property or method of the object that it uses in collation. Each call returns the index of the section array to which the model object belongs, and that value is assigned to the sectionNumber property. The data source source then creates a (temporary) outer mutable array and mutable arrays for each section; it adds each created section array to the outer array. It then enumerates the array of model objects and adds each object to its assigned section array.
2. 3.
43
CHAPTER 4
4.
The data source enumerates the array of section arrays and calls sortedArrayFromArray:collationStringSelector: on the collation manager to sort the items in each array. It passes in a section array and a property or method that is to be used in sorting the items in the array. Each sorted section array is added to the final outer array.
Now the data source is ready to populate its table view with data. It implements the methods specific to indexed lists as shown in Listing 4-8. In doing this it calls two UILocalizedIndexedCollation methods: sectionIndexTitles and sectionForSectionIndexTitleAtIndex:. Also note that in tableView:titleForHeaderInSection: it suppresses any headers from appearing in the table view when the associated section does not have any items. Listing 4-8 Providing section-index data to the table view
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView { return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles]; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if ([[self.states objectAtIndex:section] count] > 0) { return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section]; } return nil; } - (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index { return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index]; }
Finally, the data source should implement the UITableViewDataSource methods that are common to all table views. Listing 4-9 gives examples of these implementations, and illustrates how to use the section and row properties of the table viewspecific category of the NSIndexPath class described in NSIndexPath UIKit Additions. Listing 4-9 Populating the rows of an indexed list
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return [self.states count]; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [[self.states objectAtIndex:section] count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"StateCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
44
CHAPTER 4
reuseIdentifier:CellIdentifier] autorelease]; } State *stateObj = [[self.states objectAtIndex:indexPath.section] objectAtIndex:indexPath.row]; cell.textLabel.text = stateObj.name; return cell; }
After initially populating the table view following the procedure outlined above, you can thereafter reload the contents of the index by calling reloadSectionIndexTitles, a method introduced in iPhone OS 3.0. For table views that are indexed lists, when the delegate assigns cells for rows in tableView:cellForRowAtIndexPath:, it should ensure that the accessoryType property of the cell is set to UITableViewCellAccessoryNone. You can force a redisplay of the section titles of an indexed list by calling the reloadSectionIndexTitles method (available in iPhone OS 3.0).
- (void)loadView { [super loadView]; CGRect titleRect = CGRectMake(0, 0, 300, 40); UILabel *tableTitle = [[UILabel alloc] initWithFrame:titleRect]; tableTitle.textColor = [UIColor blueColor]; tableTitle.backgroundColor = [self.tableView backgroundColor]; tableTitle.opaque = YES; tableTitle.font = [UIFont boldSystemFontOfSize:18]; tableTitle.text = [curTrail objectForKey:@"Name"]; self.tableView.tableHeaderView = tableTitle; [self.tableView reloadData]; [tableTitle release]; }
The example in Listing 4-11 returns a title string for a section header. Listing 4-11 Returning a header title for a specific section
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { // Returns section title based on physical state: [solid, liquid, gas, artificial] return [[[PeriodicElements sharedPeriodicElements] elementPhysicalStatesArray] objectAtIndex:section];
45
CHAPTER 4
The code in Listing 4-12 moves a specific row to the next level of indentation. Listing 4-12 Custom indentation of a row
The example in Listing 4-13 varies the height of a specific row based on its index value. Listing 4-13 Varying row height
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { CGFloat result; switch ([indexPath row]) { case 0: { result = kUIRowHeight; break; } case 1: { result = kUIRowLabelHeight; break; } } return result; }
You can also affect the appearance of rows by returning custom UITableViewCell objects with specially formatted subviews for content in tableView:cellForRowAtIndexPath:. A Closer Look at Table-View Cells (page 47) discusses cell customization.
46
CHAPTER 5
A table view uses cell objects to draw its visible rows and caches those objects as long as the rows are visible. These objects inherit from the UITableViewCell class. The table views data source provides the cell objects to the table view by implementing the tableView:cellForRowAtIndexPath: method, a required method of the UITableViewDataSource protocol. The following sections describe the characteristics of table-view cell objects, explain how to use the default capabilities of UITableViewCell for setting cell content, and show how to create custom UITableViewCell objects.
When the table view goes into editing mode, the editing control for each cell object (if its configured to have such a control) appears on its left side in the area shown in Figure 5-2; the editing control can be either a deletion control (a red minus sign inside a circle) or an insertion control (a green plus sign inside a circle). The cells content is pushed toward the right to make room for the editing control. If the cell object is configured for reordering (that is, relocation within the table view), the reordering control appears in the right side of the cell, next to any accessory view specified for editing mode. The reordering control is a stack of horizontal lines; to relocate a row within its table view, users press on the reordering control and drag the cell. Figure 5-2 Parts of a table-view cellediting mode
Cell content Reordering control
Editing control
47
CHAPTER 5
If a cell object is reusablethe typical casethe data source assigns the cell object a reuse identifier (an arbitrary string) when it creates the cell. The table view stores the cell object in an internal queue. When the table view subsequently requests another cell object, the data source can access the queued object by sending a dequeueReusableCellWithIdentifier: message to the table view, passing in a reuse identifier. The data source simply sets the content of the cell and any special properties before returning it. This reuse of cell objects is a performance enhancement because it eliminates the overhead of cell creation. Having multiple cell objects in a queue, each with its own identifier, makes it possible to have table views constructed from cell objects of different types. For example, some rows of a table view could have content based on the image and text properties of a UITableViewCell in a predefined style while other rows could be based on a customized UITableViewCell that defines a special format for its content. When providing cells for the table view, there are three general approaches you can take. You can either use ready-made cell objects in a range of styles; you can add your own subviews to the cell objects content view (which can be done in the Interface Builder application); or you can use cell objects created from a custom subclass of UITableViewCell. Note that the content view is only a container of other views and displays no content itself.
Note: The predefined cell styles and their corresponding content properties were introduced in iPhone OS 3.0. These cell objects have two kinds of content: one or more titles (text strings) and, in some cases, an image. Figure 5-3 shows the approximate areas for image and text. As an image expands to the right, it pushes the text in the same direction. Figure 5-3 Default cell content in a UITableViewCell object
Cell content Accessory view
Image
Text
48
CHAPTER 5
The UITableViewCell class defines properties for cell content in these predefined cell styles:
textLabelThe main label for text in the cell (a UILabel object) detailTextLabelThe secondary label for text in the cell when there is additional detail (a UILabel
object)
Because the first two of these properties are labels, you can set the font, alignment, line-break mode, and color of the associated text through the properties defined by the UILabel class (including the color of text when the row is highlighted). For the image-view property, you can also set an alternate image for when the cell is highlighted using the highlightedImage property of the UIImageView class. Figure 5-4 gives an example of a table view whose rows are drawn using a UITableViewCell object in the UITableViewCellStyleSubtitle style; it includes both an image and, for textual content, a title and a subtitle. Figure 5-4 A table view with rows showing both images and text
Listing 5-1 shows the table view data source implementation of tableView:cellForRowAtIndexPath: that creates the table view in Figure 5-4 (page 49). Typically, the first thing the data source should do is send dequeueReusableCellWithIdentifier: to the table view, passing in a reuse-identifier string. If the table view does not return a reusable cell object, the data source creates one, assigning the object a reuse identifier in the final parameter of initWithStyle:reuseIdentifier:. At this point it also sets sets general properties of the cell object for the table view (in this case, its selection style). Then it sets the cell objects content, both text and image. Listing 5-1 Configuring a UITableViewCell object with both image and text
49
CHAPTER 5
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyIdentifier"]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:@"MyIdentifier"]; cell.selectionStyle = UITableViewCellSelectionStyleNone; } NSDictionary *item = (NSDictionary *)[self.content objectAtIndex:indexPath.row]; cell.textLabel.text = [item objectForKey:@"mainTitleKey"]; cell.detailTextLabel.text = [item objectForKey:@"secondaryTitleKey"]; NSString *path = [[NSBundle mainBundle] pathForResource:[item objectForKey:@"imageKey"] ofType:@"png"]; UIImage *theImage = [UIImage imageWithContentsOfFile:path]; cell.imageView.image = theImage; return cell; }
The table views data source implementation of tableView:cellForRowAtIndexPath: should always reset all content when reusing a cell. When you configure a UITableViewCell object, you also can set various other properties including (but not limited to) the following:
selectionStyleControls the appearance of the cell when selected. accessoryType and accessoryViewAllows you to set one of the standard accessory views (disclosure
indicator or detail disclosure control) or a custom accessory view for a cell in normal (non-editing) mode. For a custom view, you may provide any UIView object, such as a slider, a switch, or a custom view.
accessory views (disclosure indicator or detail disclosure control) or a custom accessory view for a cell in editing mode. For a custom view, you may provide any UIView object, such as a slider, a switch, or a custom view. (These properties were introduced in iPhone OS 3.0.)
showsReorderControlSpecifies whether it shows a reordering control when in editing mode. The related but read-only editingStyle property specifies the type of editing control the cell has (if any). The delegate returns the value of the editingStyle property in its implementation of the tableView:editingStyleForRowAtIndexPath: method. backgroundView and selectedBackgroundViewProvides a background view (when a cell is
unselected and selected) to display behind all other views of the cell.
indentationLevel and indentationWidthSpecifies the indentation level for cell content and the
width of each indentation level. Because a table-view cell inherits from UIView, you can also affect its appearance and behavior by setting the properties defined by that superclass. For example, to affect the background color a cell, you could set its backgroundColor property. Listing 5-2 shows how you might alternate the background color of rows (via their backing cells) in a table view. Listing 5-2 Alternating the background color of cells in
tableView:willDisplayCell:forRowAtIndexPath:
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0 || indexPath.row%2 == 0) {
50
CHAPTER 5
Listing 5-2 also illustrates an important aspect of the table-view API. A table view sends a tableView:willDisplayCell:forRowAtIndexPath: message to its delegate just before it draws a row. If the delegate chooses to implement this method, it can make last-minute changes to the cell object before it is displayed. In this method the delegate should change state-based properties set earlier by the table view, such as selection and background color, and not content.
Customizing Cells
UITableViewCell objects in their various predefined styles suffice for most of the rows that table views
display. With these ready-made cell objects, rows can include one or two styles of text, often an image, and an accessory view of some sort. The application can modify the text in its font, color, and other characteristics, and it can supply an image for the row in its selected state as well as its normal state. However, as flexible and useful as this cell content is, it might not satisfy the requirements of all applications. For example, the labels permitted by a native UITableViewCell object are pinned to specific locations within a row, and the image must appear on the left side of the row. If you want the cell to have different content components and to have these laid out in different locations, or if you want different behavioral characteristics for the cell, you have two alternatives. You can add subviews to the contentView property of the cell object or you can create a custom subclass of UITableViewCell.
You should add subviews to a cells content view when your content layout can be specified entirely with the appropriate autoresizing settings and when you dont need to modify the default behavior of the cell. You should create a custom subclass when your content requires custom layout code or when you need to change the default behavior of the cell, such as in response to editing mode.
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
51
CHAPTER 5
Lets say you wanted a cell with text and image content in entirely different locations than those provided by the standard cell styles. For example, you want the image on the right side of the cell and the title and subtitle of the cell right-aligned against the left side of the image. Figure 5-5 show how a table view with rows drawn with such a cell might look. (This example is for illustration only, and is not intended as a human-interface model.) Figure 5-5 Cells with custom content as subviews
The code example in Listing 5-3 illustrates how the data source programmatically composes the cell with which this table view draws its rows. In tableView:cellForRowAtIndexPath:, it first checks to see the table view already has a cell object with the given reuse identifier. If there is no such object, the data source creates creates two label objects and one image view with specific frames within the coordinate system of their superview (the content view). It also sets attributes of these objects. Nextor if the cell already exists in the table views queuethe data source sets the cells content before returning the cell. Listing 5-3 Adding subviews to a cells content view
#define MAINLABEL_TAG 1 #define SECONDLABEL_TAG 2 #define PHOTO_TAG 3 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"ImageOnRightCell"; UILabel *mainLabel, *secondLabel; UIImageView *photo; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
52
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton; mainLabel = [[[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, 220.0, 15.0)] autorelease]; mainLabel.tag = MAINLABEL_TAG; mainLabel.font = [UIFont systemFontOfSize:14.0]; mainLabel.textAlignment = UITextAlignmentRight; mainLabel.textColor = [UIColor blackColor]; mainLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight; [cell.contentView addSubview:mainLabel]; secondLabel = [[[UILabel alloc] initWithFrame:CGRectMake(0.0, 20.0, 220.0, 25.0)] autorelease]; secondLabel.tag = SECONDLABEL_TAG; secondLabel.font = [UIFont systemFontOfSize:12.0]; secondLabel.textAlignment = UITextAlignmentRight; secondLabel.textColor = [UIColor darkGrayColor]; secondLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight; [cell.contentView addSubview:secondLabel]; photo = [[[UIImageView alloc] initWithFrame:CGRectMake(225.0, 0.0, 80.0, 45.0)] autorelease]; photo.tag = PHOTO_TAG; photo.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleHeight; [cell.contentView addSubview:photo]; } else { mainLabel = (UILabel *)[cell.contentView viewWithTag:MAINLABEL_TAG]; secondLabel = (UILabel *)[cell.contentView viewWithTag:SECONDLABEL_TAG]; photo = (UIImageView *)[cell.contentView viewWithTag:PHOTO_TAG]; } NSDictionary *aDict = [self.list objectAtIndex:indexPath.row]; mainLabel.text = [aDict objectForKey:@"mainTitleKey"]; secondLabel.text = [aDict objectForKey:@"secondaryTitleKey"]; NSString *imagePath = [[NSBundle mainBundle] pathForResource:[aDict objectForKey:@"imageKey"] ofType:@"png"]; UIImage *theImage = [UIImage imageWithContentsOfFile:imagePath]; photo.image = theImage; return cell; }
Note that when the data source creates the cells, it assigns each subview a tag. A tag is an identifier of a view; it allows you to locate a view in its view hierarchy by calling the viewWithTag: method. If later the delegate gets the designated cell from the table views queue, it uses the tags to obtain references to the three subviews prior to assigning them content. Also note that this code creates a UITableViewCell object in the predefined default style (UITableViewCellStyleDefault). However, because the content properties of the standard cellstextLabel, detailTextLabel, and imageVieware nil until assigned content, you may use any predefined cell as the template for customization.
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
53
CHAPTER 5
Note: An alternative, and perhaps easier, approach for programmatically composing the same cells shown in Figure 5-5 (page 52) is to subclass UITableViewCell and create instances in the UITableViewCellStyleSubtitle style. Then override the layoutSubviews method to reposition the textLabel, detailTextLabel, and imageView subviews (after calling super). A common technique for achieving attributed string effects with textual content is to lay out UILabel subviews of the UITableViewCell content view. The text of each label can have its own font, color, size, alignment, and other characteristics. There can be no variation of textual attributes within a label object, so you must create multiple labels and lay them out relative to each other if you want that kind of variation within a cell.
54
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
Figure 5-6
In the view controller that manages the table view (typically a UITableViewController object), define an outlet property for the customized table-view cell you are going to load from the nib file, as shown in Listing 5-4. Make sure you synthesize accessor methods for the property in the implementation file. Listing 5-4 Defining an outlet for the cell
@interface TVController : UITableViewController { UITableViewCell *tvCell; } @property (nonatomic, assign) IBOutlet UITableViewCell *tvCell; @end
In Interface Builder complete the following steps: 1. Create a nib file. To this, choose New from the File menu, select the Empty template, and click Choose. 2. Save the nib file under an appropriate name and, when prompted, add it to the project. This name is what you specify as the first argument of the loadNibNamed:owner:options: method call that loads the nib file from the applications main bundle. See Listing 5-5 (page 57) for a code example. 3. Drag a Table View Cell object from the Library into the nib document window.
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
55
CHAPTER 5
4.
Drag objects from the Library onto the content view. For this example, drag two label objects and position them near the ends of each cell (leaving room for the accessory view).
5.
Select the objects on the content view and set their attributes, sizes, and autoresizing characteristics. An important attribute to set for the programmatic portion of this procedure is each objects tag property. Find this property in the View section of the Attributes pane and assign each object a unique integer.
6.
Select the cell itself and set any general attributes you want it to have, such as alignment, font size, color, line-break mode, and so on.
Always set a string as an identifier of the cell; the table view requires the identifier to fetch a cell from its cache, if its present. Setting the identifier is especially important if you have more than one kind of cell for a table view. You may also set the height and autoresizing characteristics of the cell at this time. 7. 8. Select Files Owner in the nib document window, open the Identity pane of the inspector, and set the class of Files Owner to your custom view controller class. Connect the cell outlet of Files Owner (now the placeholder instance of your custom subclass) to the table-view cell object in the nib-file document.
56
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
Now save the nib file and return to the Xcode project. Write all the code you normally to obtain the table-views data and set up the table view. Implement the data source methods as you normally would, except for tableView:cellForRowAtIndexPath:. Implement that in a manner similar to the example in Listing 5-5. Listing 5-5 Loading a cell from a nib file and assigning it content
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *MyIdentifier = @"MyIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if (cell == nil) { [[NSBundle mainBundle] loadNibNamed:@"TVCell" owner:self options:nil]; cell = tvCell; self.tvCell = nil; } UILabel *label; label = (UILabel *)[cell viewWithTag:1]; label.text = [NSString stringWithFormat:@"%d", indexPath.row]; label = (UILabel *)[cell viewWithTag:2]; label.text = [NSString stringWithFormat:@"%d", NUMBER_OF_ROWS indexPath.row]; return cell; }
The string identifier you assigned to the cell in Interface Builder is the same string passed to the table view in dequeueReusableCellWithIdentifier:. The cell is loaded from the nib file as required to fill the table view. If rows are scrolled out of view, the associated cells become available in the table views cache. Keep in mind that the table view may request additional cellsthat is, more than are present in the reuse queueat any time for a variety of reasons. For example, if you insert a hundred new rows, the table view might ask for a hundred new cells at once, none of which will be kept in the reuse queue until after the insertion operation completes.
The nib file is loaded using the NSBundle method loadNibNamed:owner:options:, which passes self as the owner (remember, Files Owner refers to your table-view controller). Because of the outlet connection you made, the tvCell outlet now has a reference to the cell loaded from the nib file. Immediately assign the cell to the passed-in cell variable and set the outlet to nil. The code gets the labels in the cell by calling viewWithTag:, passing in their tag integers. It can then set the textual content of the labels.
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
57
CHAPTER 5
Figure 5-7
Table view rows drawn with multiple cells from a nib vile
As with the procedure for dynamic content, start by adding a subclass of UITableViewController to your project. (See Adding Table Views to the Application (page 38) for details.) Define outlet properties for each of the three cells in the nib file plus an outlet property for the slider-value label in the last cell, as shown in Listing 5-6. Listing 5-6 Defining outlet properties for the cells in the nib file
@interface MyTableViewController : UITableViewController { UITableViewCell *cell0; UITableViewCell *cell1; UITableViewCell *cell2; UILabel *cell2Label; } @property @property @property @property (nonatomic, (nonatomic, (nonatomic, (nonatomic, retain) retain) retain) retain) IBOutlet IBOutlet IBOutlet IBOutlet UITableViewCell *cell0; UITableViewCell *cell1; UITableViewCell *cell2; UILabel *cell2Label;
Start by creating a nib file that contains a table view and make your custom table-view controller Files Owner of the nib file. Connect the view outlet of the controller to the table view and change the style of the table view to Grouped in the Attributes pane of the inspector. (Creating a Table View Application the Easy Way (page 35) describes how to do these things.) Then for the cells, complete the following steps: 1. Drag three Table View Cell objects from the Interface Builder Library into the nib document window.
58
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
2.
Drag objects from the Library to compose the subviews of each cell as depicted here:
It is not necessary to assign identifiers as attributes of these cells because they are single-use cells. 3. Set any desired attributes of these objects. For example, the slider should have a range of values from 0 to 10 with an initial value of 7.5. 4. Right-click (or Control-click) Files Owner in the nib document window to display the connections window; make outlet connections between Files Owner (your table-view controller) and each of the cell objects.
Also connect the cell2Label outlet to the slider-value label. Note that giving views like these tags is not necessary with static content because you can make outlet connections to them.
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
59
CHAPTER 5
5.
While youre at it, implement the two action methods declared in Listing 5-6 (page 58) and make target-action connections as shown in the above illustration.
Save the nib file, return to the Xcode project, and implement the data source methods for the table view. When the application delegate or previous table-view controller instantiates the current table-view controller, the nib file containing the table view and the table-view cells is loaded into application memory. Because the cells in the nib file are single-use cells, you need only return them to the table view (via their outlets) when it asks for them in the tableView:cellForRowAtIndexPath: method, as shown in Listing 5-7. Listing 5-7 Passing nib-file cells to the table view
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section == 0) { return cell0; } // section 1 if (indexPath.row == 0) { return cell1; } return cell2; }
Even though the cells in the nib file are for static content, with each cell used only once in the table view, you can still dynamically modify their contents. For example, you could easily change the text value of the label in the first cell to a property of the current data item; Listing 5-8 illustrates how this might look. Listing 5-8 Dynamically changing the content of a nib-file cell
// ... if (indexPath.section == 0) { UILabel *theLabel = (UILabel *)[cell0 viewWithTag:1]; theLabel.text = [self.currentAddress lastName]; return cell0; } // ...
Subclassing UITableViewCell
Another way of customizing the appearance of cell objects is to create a custom subclass of UITableViewCell that draws its own content. You then use instances of this class to populate the rows of a table view. This approach also gives you greater control over how the cells behave, for instance, when the table view enters editing mode. (In editing mode, the area for content shrinks.) Figure 5-8 gives an example of a custom table-view cell object. Figure 5-8 A custom table-view cell
60
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
Before you write the first line of subclassing code, carefully consider some some design aspects and performance constraints of UITableViewCell subclasses.
Draw the entire cell only when appropriate. Your subclass of UITableViewCell could draw all of its content in its drawRect: method, but you should be aware of the potential drawbacks of this approach. Custom drawing applies to the cells layer, which can be obscured by any views placed over it. For example, in table views in the grouped style, the background view (the backgroundView property) obscures any drawing performed in drawRect:. The blue selection background will also obscure any drawing. Moreover, custom drawing that occurs during animation (such as when the table view enters and exits editing mode) drastically decreases performance. An alternative is a subclass that composes the content of the cell from subviews, laying those views out in the desired way. Because those views are cached, they can simply be moved around (when, for instance, the cell goes into editing mode). Programmatically Adding Subviews to a Cells Content View (page 51) illustrates one such approach and notes another. However, if the content of a cell is composed of more than three or four subviews, scrolling performance might suffer. In this case (and especially if the cell is not editable), consider drawing directly in one subview of the cells content view. The gist of this guideline is that, when implementing custom table-view cells, be aware that there is a tradeoff between optimal scrolling performance and optimal editing or reordering performance.
Avoid transparency. Subviews of table-view cells have a compositing cost that you can largely mitigate by making the views opaque. Even one transparent subview per cell impacts scrolling performance. Always use opaque subviews if at all possible. Mark the cell as needing display when viewable properties change. If you have a custom reusable table cell and it displays a custom property as part of the cell content, you must be sure to send a setNeedsDisplay message to the cell if the value of the property changes. Otherwise UIKit doesnt know that the cell is dirty and therefore wont invoke the cells drawRect: method to have it redraw itself with the new value. A good place to call setNeedsDisplay is in a (non-synthesized) setter method associated with the property.
The remainder of this section takes you on a guided tour of a the parts of the CustomTableViewCell project that implement a custom subclass of UITableViewCell. (This project is part of the TableViewSuite extended example.) This subclass implements a cell with complex content that, because it is complex, has a single custom view that draws itself. By examining how this project creates the custom cell object shown in Figure 5-8 (page 60), you can gain a working understanding of how you might create your own custom subclasses of UITableViewCell. The CustomTableViewCell project declares the interface of the TimeZoneCell subclass of UITableViewCell as shown in Listing 5-9. This interface is simple, consisting of a reference to a custom view class and two methods, one for setting the content that the custom view uses to draw and the other for redrawing the cell on demand. Listing 5-9 Declaring the properties and methods of the TimeZoneCell class
@class TimeZoneWrapper; @class TimeZoneView; @interface TimeZoneCell : UITableViewCell { TimeZoneView *timeZoneView; } @property (nonatomic, retain) TimeZoneView *timeZoneView; - (void)setTimeZoneWrapper:(TimeZoneWrapper *)newTimeZoneWrapper;
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
61
CHAPTER 5
- (void)redisplay; @end
The method setTimeZoneWrapper: takes as an argument a custom model object that represents a time zone and lazily creates and caches derived properties that are expensive to compute. The TimeZoneWrapper class is important because an instance of that class is the source for each cells content. (The implementation of the class is not shown here.) In its implementation, the TimeZoneCell class overrides the initWithStyle:reuseIdentifier: initializer of its superclass, calling the superclass implementation as the first step. In this method it creates an instance of the TimeZoneView class sized to the bounds of the cells content view. It sets the autoresizing characteristics of the view and adds it as a subview of the cells content view. Listing 5-10 shows the initialization code. Listing 5-10 Initializing an instance of TimeZoneCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier]) { CGRect tzvFrame = CGRectMake(0.0, 0.0, self.contentView.bounds.size.width, self.contentView.bounds.size.height); timeZoneView = [[TimeZoneView alloc] initWithFrame:tzvFrame]; timeZoneView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; [self.contentView addSubview:timeZoneView]; } return self; }
The TimeZoneView class has an interface as shown in Listing 5-11. In addition to the instance of TimeZoneWrapper, it encapsulates a date formatter, an abbreviation, and two flags for indicating whether the cell is highlighted and whether editing mode is effect. Listing 5-11 Declaring the interface of the TimeZoneView class
@interface TimeZoneView : UIView { TimeZoneWrapper *timeZoneWrapper; NSDateFormatter *dateFormatter; NSString *abbreviation; BOOL highlighted; BOOL editing; } @property (nonatomic, retain) TimeZoneWrapper *timeZoneWrapper; @property (nonatomic, retain) NSDateFormatter *dateFormatter; @property (nonatomic, retain) NSString *abbreviation; @property (nonatomic, getter=isHighlighted) BOOL highlighted; @property (nonatomic, getter=isEditing) BOOL editing; @end
Recall that the TableViewCell class declared a method for setting a TimeZoneWrapper object. This method simply invokes the identical setter method (non-synthesized) of the TimeZoneView class encapsulated by TimeZoneCell. That setter method is implemented as shown in Listing 5-12. In addition to providing the standard memory-management code, this setter method associates the time zone with the data formatter, creates an abbreviation for the time zone, and marks the view for redisplay.
62
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
Listing 5-12
- (void)setTimeZoneWrapper:(TimeZoneWrapper *)newTimeZoneWrapper { // If the time zone wrapper changes, update the date formatter and abbreviation string. if (timeZoneWrapper != newTimeZoneWrapper) { [timeZoneWrapper release]; timeZoneWrapper = [newTimeZoneWrapper retain]; [dateFormatter setTimeZone:timeZoneWrapper.timeZone]; NSString *string = [[NSString alloc] initWithFormat:@"%@ (%@)", timeZoneWrapper.abbreviation, timeZoneWrapper.gmtOffset]; self.abbreviation = string; [string release]; } [self setNeedsDisplay]; }
After the TimeZoneView class is marked for redisplay its drawRect: method is invoked. Listing 5-13 shows representative sections of the TimeZoneView implementation, eliding other parts for brevity. One of these elided parts is the initial code that defines both constants for laying out the fields of the view and colors for drawn text that is conditional on whether the cell is in a normal or highlighted state. The implementation uses the drawAtPoint:forWidth:withFont:fontSize:lineBreakMode:baselineAdjustment: method of NSString to draw the text and the drawAtPoint: method of UIImage to draw the image. Listing 5-13 Drawing the custom table-view cell
- (void)drawRect:(CGRect)rect { // set up #define constants and fonts here ... // set up text colors for main and secondary text in normal and highlighted cell states... CGRect contentRect = self.bounds; if (!self.editing) { CGFloat boundsX = contentRect.origin.x; CGPoint point; CGFloat actualFontSize; CGSize size; // draw main text [mainTextColor set]; // draw time-zone locale string point = CGPointMake(boundsX + LEFT_COLUMN_OFFSET, UPPER_ROW_TOP); [timeZoneWrapper.timeZoneLocaleName drawAtPoint:point forWidth:LEFT_COLUMN_WIDTH withFont:mainFont minFontSize:MIN_MAIN_FONT_SIZE actualFontSize:NULL lineBreakMode:UILineBreakModeTailTruncation baselineAdjustment:UIBaselineAdjustmentAlignBaselines]; // ... other strings drawn here... // draw secondary text [secondaryTextColor set]; // draw the time-zone abbreviation point = CGPointMake(boundsX + LEFT_COLUMN_OFFSET, LOWER_ROW_TOP);
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
63
CHAPTER 5
[abbreviation drawAtPoint:point forWidth:LEFT_COLUMN_WIDTH withFont:secondaryFont minFontSize:MIN_SECONDARY_FONT_SIZE actualFontSize:NULL lineBreakMode:UILineBreakModeTailTruncation baselineAdjustment:UIBaselineAdjustmentAlignBaselines]; // ... other strings drawn here... // Draw the quarter image. CGFloat imageY = (contentRect.size.height timeZoneWrapper.image.size.height) / 2; point = CGPointMake(boundsX + RIGHT_COLUMN_OFFSET, imageY); [timeZoneWrapper.image drawAtPoint:point]; } }
An important aspect of this drawRect: implementation is the if statement that checks whether the cells table view is in editing mode. The view draws the content of its cell only if the cell is not in editing mode. If you wished, you could add an else clause to this statement that draws the cell when it is in editing mode; because the cell has a reduced content area in editing mode, you might have to move fields around, shrink font sizes, or even omit less-important fields. However, drawing in editing mode is not encouraged because, as you might recall, custom drawing while cells animate into and out of editing mode severely affects performance. Finally, the data source provides its table view with the custom cell in the tableView:cellForRowAtIndexPath: method, as shown in Listing 5-14. For the cell content, it locates and sets the TimeZoneWrapper object that corresponds to the corresponding section and row of the table view. Listing 5-14 Returning an initialized instance of the custom table-view cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"TimeZoneCell"; TimeZoneCell *timeZoneCell = (TimeZoneCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (timeZoneCell == nil) { timeZoneCell = [[[TimeZoneCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; timeZoneCell.frame = CGRectMake(0.0, 0.0, 320.0, ROW_HEIGHT); } Region *region = [displayList objectAtIndex:indexPath.section]; NSArray *regionTimeZones = region.timeZoneWrappers; [timeZoneCell setTimeZoneWrapper:[regionTimeZones objectAtIndex:indexPath.row]]; return timeZoneCell; }
A subclass of UITableViewCell may override the method prepareForReuse to reset attributes of the cell object. The table view invokes this method just before it returns a cell object to the data source in dequeueReusableCellWithIdentifier:. For performance reasons, you should only reset attributes of the cell that are not related to content, for example, alpha, editing, and selection state.
64
Customizing Cells
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 5
Reuse cells. Object allocation has a performance cost, especially if the allocation has to happen repeatedly over a short periodsay, when the user scrolls a table view. If you reuse cells instead of allocating new ones, you greatly enhance table-view performance. Avoid relayout of content. When reusing cells with custom subviews, refrain from laying out those subviews each time the table view requests a cell. Lay out the subviews once, when the cell is created. Use opaque subviews. When customizing table view cells, make the subviews of the cell opaque, not transparent.
65
CHAPTER 5
66
CHAPTER 6
Managing Selections
When users tap a row of a table view, usually something happens as a result. Another table view could slide into place, the row could display a checkmark, or some other action could be performed. The following sections describe how to respond to selections and how to make selections programmatically.
You should never use selection to indicate state. Instead, use check marks and accessory views for showing state. When the user selects a cell, you should respond by deselecting the previously selected cell (by calling the deselectRowAtIndexPath:animated: method) as well as by performing any appropriate action, such as displaying a detail view. If you respond to the the selection of a cell by pushing a new view controller onto the navigation controllers stack, you should deselect the cell (with animation) when the view controller is popped off the stack.
You can control whether rows are selectable when the table view is in editing mode by setting the allowsSelectionDuringEditing property of UITableView. In addition, beginning with iPhone OS 3.0, you can control whether cells are selectable when editing mode is not in effect by setting the allowsSelection property.
Responding to Selections
Users tap a row in a table view either to signal to the application that they want to know more about what that row signifies or to select what the row represents. In response to the user tapping a row, an application could do any of the following:
Show the next level in a data-model hierarchy. Show a detail view of an item (that is, a leaf node of the data-model hierarchy). Show a checkmark in the row to indicate that the represented item is selected. If the touch occurred in a control embedded in the row, it could respond to the action message sent by the control.
To handle most selections of rows, the table views delegate must implement the tableView:didSelectRowAtIndexPath: method. In sample method implementation shown in Listing 6-1, the delegate first deselects the selected row. Then it allocates and initializes an instance of the next
67
CHAPTER 6
Managing Selections
table-view controller in the sequence. It sets the data this view controller needs to populate its table view and then pushes this object onto the stack maintained by the applications UINavigationController object. Listing 6-1 Responding to a row selection
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; BATTrailsViewController *trailsController = [[BATTrailsViewController alloc] initWithStyle:UITableViewStylePlain]; trailsController.selectedRegion = [regions objectAtIndex:indexPath.row]; [[self navigationController] pushViewController:trailsController animated:YES]; [trailsController release]; }
If a row has a disclosure controlthe white chevron over a blue circlefor an accessory view, clicking the control results in the delegate receiving a tableView:accessoryButtonTappedForRowWithIndexPath: message (instead of tableView:didSelectRowAtIndexPath:). The delegate responds to this message in the same general way as it does for other kinds of selections. A row can also have a control object as its accessory view, such as a switch or a slider. This control object functions as it would in any other context: Manipulating the object in the proper way results in an action message being sent to a target object. Listing 6-2 illustrates a data source object that adds a UISwitch object as a cells accessory view and then responds to the action messages sent when the switch is flipped. Listing 6-2 Setting a switch object as an accessory view and responding to its action message
- (UITableViewCell *)tableView:(UITableView *)theTableView } UITableViewCell *cell = [tv dequeueReusableCellWithIdentifier:@"CellWithSwitch"]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellWithSwitch"] autorelease]; cell.selectionStyle = UITableViewCellSelectionStyleNone; cell.textLabel.font = [UIFont systemFontOfSize:14]; } UISwitch *switchObj = [[UISwitch alloc] initWithFrame:CGRectMake(1.0, 1.0, 20.0, 20.0)]; switchObj.on = YES; [switchObj addTarget:self action:@selector(toggleSoundEffects:) forControlEvents:(UIControlEventValueChanged | UIControlEventTouchDragInside)]; cell.accessoryView = switchObj; [switchObj release]; cell.textLabel.text = @"Sound Effects"; return cell; - (void)toggleSoundEffects:(id)sender { [self.soundEffectsOn = [(UISwitch *)sender isOn]; [self reset]; }
68
Responding to Selections
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 6
Managing Selections
You may also define controls as accessory views of table-view cells created in Interface Builder. Drag a control object (switch, slider, and so on) into a nib document window containing a table-view cell. Then, using the connection window, make the control the accessory view of the cell. Loading Custom Table-View Cells From Nib Files (page 54) describes the procedure for creating and configuring table-view cell objects in nib files. Selection management is also important with selection lists. There are two kinds of selection lists:
Exclusive lists where only one row is permitted the checkmark Inclusive lists where more than one row can have a checkmark
Listing 6-3 illustrates one approach to managing an exclusive selection list. It first deselects the currently selected row and returns if the same row is selected; otherwise it sets the checkmark accessory type on the newly selected row and removes the checkmark on the previously selected row Listing 6-3 Managing a selection listexclusive list
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [tableView deselectRowAtIndexPath:indexPath animated:NO]; NSInteger catIndex = [taskCategories indexOfObject:self.currentCategory]; if (catIndex == indexPath.row) { return; } NSIndexPath *oldIndexPath = [NSIndexPath indexPathForRow:catIndex inSection:0]; UITableViewCell *newCell = [tableView cellForRowAtIndexPath:indexPath]; if (newCell.accessoryType == UITableViewCellAccessoryNone) { newCell.accessoryType = UITableViewCellAccessoryCheckmark; self.currentCategory = [taskCategories objectAtIndex:indexPath.row]; } UITableViewCell *oldCell = [tableView cellForRowAtIndexPath:oldIndexPath]; if (oldCell.accessoryType == UITableViewCellAccessoryCheckmark) { oldCell.accessoryType = UITableViewCellAccessoryNone; } }
Listing 6-4 illustrates how to manage a inclusive selection list. As the comments in this example indicate, when the delegate adds a checkmark to a row or removes one, it typically also sets or unsets any associated model-object attribute. Listing 6-4 Managing a selection listinclusive list
- (void)tableView:(UITableView *)theTableView didSelectRowAtIndexPath:(NSIndexPath *)newIndexPath { [theTableView deselectRowAtIndexPath:[theTableView indexPathForSelectedRow] animated:NO]; UITableViewCell *cell = [theTableView cellForRowAtIndexPath:newIndexPath]; if (cell.accessoryType == UITableViewCellAccessoryNone) { cell.accessoryType = UITableViewCellAccessoryCheckmark; // Reflect selection in data model } else if (cell.accessoryType == UITableViewCellAccessoryCheckmark) { cell.accessoryType = UITableViewCellAccessoryNone;
Responding to Selections
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
69
CHAPTER 6
Managing Selections
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)newIndexPath { NSIndexPath *scrollIndexPath; if (newIndexPath.row + 20 < [timeZoneNames count]) { scrollIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row+20 inSection:newIndexPath.section]; } else { scrollIndexPath = [NSIndexPath indexPathForRow:newIndexPath.row-20 inSection:newIndexPath.section]; } [theTableView selectRowAtIndexPath:scrollIndexPath animated:YES scrollPosition:UITableViewScrollPositionMiddle]; }
70
CHAPTER 7
A table view has an editing mode as well as its normal (selection) mode. When a table view goes into editing mode, it displays the editing and reordering controls associated with its rows. The editing controls, which are in the left side of the row, allow the user to insert and delete rows in the table view. The editing controls have distinctive appearances: Deletion control
Insertion control
When a table view enters editing mode and when users click an editing control, the table view sends a series of messages to its data source and delegate, but only if they implement these methods. These methods allow the data source and delegate to refine the appearance and behavior of rows in the table view; the messages also enable them to carry out the deletion or insertion operation. Even if a table view is not in editing mode, you can insert or delete a number of rows or sections as a group and have those operations animated. The first section below shows you how, when a table is in editing mode, to insert new rows and delete existing rows in a table view in response to user actions. The second section, Batch Insertion and Deletion of Rows and Sections (page 76), discusses how you can insert and delete multiple sections and rows animated as a group.
71
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
CHAPTER 7
Note: The procedure for reordering rows when in editing mode is described in Managing the Reordering of Rows (page 79).
Data Source
tableView: canEditRowAtIndexPath: User presses editing control User presses Delete button tableView:commitEditingStyle: forRowAtIndexPath: deleteRowsAtIndexPath: withRowAnimation or insertRowsAtIndexPath: withRowAnimation:
tableView: editingStyleForRowAtIndexPath:
72
CHAPTER 7
After resending setEditing:animated: to the cells corresponding to the visible rows, the sequence of messages is as follows: 1. The table view invokes the tableView:canEditRowAtIndexPath: method if its data source implements it. This method allows the application to exclude rows in the table view from being edited even when their cells editingStyle property indicates otherwise. Most applications do not need to implement this method. The table view invokes the tableView:editingStyleForRowAtIndexPath: method if its delegate implements it. This method allows the application to specify a rows editing style and thus the editing control that the row displays. At this point, the table view is fully in editing mode. It displays the insertion or deletion control for each eligible row. 3. The user taps an editing control (either the deletion control or the insertion control). If he or she taps a deletion control, a Delete button is displayed on the row. The user then taps that button to confirm the deletion. The table view sends the tableView:commitEditingStyle:forRowAtIndexPath: message to the data source. Although this protocol method is marked as optional, the data source must implement it if it wants to insert or delete a row. It must do two things:
2.
4.
Send deleteRowsAtIndexPaths:withRowAnimation: or insertRowsAtIndexPaths:withRowAnimation: to the table view to direct it to adjust its presentation. Update the corresponding data-model array by either deleting the referenced item from the array or adding an item to the array.
When the user swipes across a row to display the Delete button for that row, there is a variation in the calling sequence diagrammed in Figure 7-1 (page 72). When the user swipes a row to delete it, the table view first checks to see if its data source has implemented the tableView:commitEditingStyle:forRowAtIndexPath: method; if that is so, it sends setEditing:animated: to itself and enters editing mode. In this swipe to delete mode, the table view does not display the editing and reordering controls. Because this is a user-driven event, it also brackets the messages to the delegate inside of two other messages: tableView:willBeginEditingRowAtIndexPath: and tableView:didEndEditingRowAtIndexPath:. By implementing these methods, the delegate can update the appearance of the table view appropriately. Note: The data source should not call setEditing:animated: from within its implementation of tableView:commitEditingStyle:forRowAtIndexPath:. If for some reason it must, it should invoke it after a delay by using the performSelector:withObject:afterDelay: method. Although you can use an insertion control as the trigger to insert a new row in a table view, an alternative approach is to have an Add (or plus sign) button in the navigation bar. Tapping the button sends an action message to the view controller, which overlays the table view with a modal view for entering the new item. Once the item is entered, the controller adds it to the data-model array and reloads the table. An Example of Adding a Table-View Row (page 75) discusses this approach.
73
CHAPTER 7
This button is preconfigured to send setEditing:animated: to the view controller when tapped; it toggles the button title (between Edit and Done) and the Boolean editing parameter on alternating taps. In its implementation of the method, as shown in Listing 7-1, the view controller invokes the superclass invocation of the method, sends the same message to the table view, and updates the enabled state of the other button in the navigation bar (a plus-sign button, for adding items). Listing 7-1 View controller responding to setEditing:animated:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated { [super setEditing:editing animated:animated]; [tableView setEditing:editing animated:YES]; if (editing) { addButton.enabled = NO; } else { addButton.enabled = YES; } }
When its table view enters editing mode, the view controller specifies a deletion control for every row except the last, which has an insertion control. It does this in its implementation of the tableView:editingStyleForRowAtIndexPath: method (Listing 7-2). Listing 7-2 Customizing the editing style of rows
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate]; if (indexPath.row == [controller countOfList]-1) { return UITableViewCellEditingStyleInsert; } else { return UITableViewCellEditingStyleDelete; } }
The user taps the deletion control in a row and the view controller receives a tableView:commitEditingStyle:forRowAtIndexPath: message from the table view. As shown in Listing 7-3, it handles this message by removing the item corresponding to the row from a model array and sending deleteRowsAtIndexPaths:withRowAnimation: to the table view. Listing 7-3 Updating the data-model array and deleting the row
74
CHAPTER 7
// If row is deleted, remove it from the list. if (editingStyle == UITableViewCellEditingStyleDelete) { SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate]; [controller removeObjectFromListAtIndex:indexPath.row]; [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade]; } }
Note that the view controller sets the control states for the title as well as the action selector and the target object. When the user taps the Add button, the addItem: message is sent to the target (the view controller). It responds as shown in Listing 7-5. It creates a navigation controller with a single view controller whose view is put onscreen modallyit animates upward to overlay the table view. The presentModalViewController:animated: method to do this. Listing 7-5 Responding to a tap on the Add button
- (void)addItem:sender { if (itemInputController == nil) { itemInputController = [[ItemInputController alloc] init]; } UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:itemInputController]; [[self navigationController] presentModalViewController:navigationController animated:YES]; [navigationController release]; }
The modal, or overlay, view consists of a single custom text field. The user enters text for the new table-view item and then taps a Save button. This button sends a save: action message to its target: the view controller for the modal view. As shown in Listing 7-6, the view controller extracts the string value from the text field and updates the applications data-model array with it. Listing 7-6 Adding the new item to the data-model array
- (void)save:sender {
75
CHAPTER 7
UITextField *textField = [(EditableTableViewTextField *)[tableView cellForRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:0]] textField]; SimpleEditableListAppDelegate *controller = (SimpleEditableListAppDelegate *)[[UIApplication sharedApplication] delegate]; NSString *newItem = textField.text; if (newItem != nil) { [controller insertObject:newItem inListAtIndex:[controller countOfList]]; } [self dismissModalViewControllerAnimated:YES]; }
After the modal view is dismissed the table view is reloaded, and it now reflects the added item.
- (void)beginUpdates; - (void)endUpdates; - (void)insertSections:(NSIndexSet *)sections withRowAnimation: (UITableViewRowAnimation)animation; - (void)deleteSections:(NSIndexSet *)sections withRowAnimation: (UITableViewRowAnimation)animation; - (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation; - (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation;
To animate a batch insertion and deletion of rows and sections, call the insertion and deletion methods within an animation block defined by successive calls to beginUpdates and endUpdates. If you dont call the insertion and deletion methods within this block, row and section indexes may be invalid. beginUpdates...endUpdates blocks are not nestable. At the conclusion of a blockthat is, after endUpdates returnsthe table view queries its data source and delegate as usual for row and section data. Thus the collection objects backing the table view should be updated to reflect the new or removed rows or sections. The reloadSections:withRowAnimation: and reloadRowsAtIndexPaths:withRowAnimation: methods, which were introduced in iPhone OS 3.0, are related to the methods discussed above. They allow you to request the table view to reload the data for specific sections and rows instead of loading the entire visible table view by calling reloadData.
76
CHAPTER 7
- (IBAction)insertAndDeleteRows:(id)sender { // original rows: Arizona, California, Delaware, New Jersey, Washington [states [states [states [states [states removeObjectAtIndex:4]; // Washington removeObjectAtIndex:2]; // Delaware insertObject:@"Alaska" atIndex:0]; insertObject:@"Georgia" atIndex:3]; insertObject:@"Virginia" atIndex:5];
NSArray *deleteIndexPaths = [NSArray arrayWithObjects: [NSIndexPath indexPathForRow:2 [NSIndexPath indexPathForRow:4 nil]; NSArray *insertIndexPaths = [NSArray arrayWithObjects: [NSIndexPath indexPathForRow:0 [NSIndexPath indexPathForRow:3 [NSIndexPath indexPathForRow:5 nil]; UITableView *tv = (UITableView *)self.view; [tv beginUpdates]; [tv insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight]; [tv deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade]; [tv endUpdates];
inSection:0], inSection:0],
This example removes two strings from an array (and their corresponding rows) and inserts three strings into the array (along with their corresponding rows). The next section, Ordering of Operations and Index Paths, explains particular aspects of the row (or section) insertion and deletion behavior.
77
CHAPTER 7
Deletions within an animation block specify which rows and sections in the original table should be removed; insertions specify which rows and sections should be added to the resulting table. The index paths used to identify sections and rows follow this model. Inserting or removing an item in a mutable array, on the other hand, may affect the array index used for the successive insertion or removal operation; for example, if you insert an item at a certain index, the indexes of all subsequent items in the array are incremented. An example is useful here. Say you have a table view with three sections, each with three rows. Then you implement the following animation block: 1. 2. 3. 4. 5. Begin updates. Delete row at index 1 of section at index 0. Delete section at index 1. Insert row at index 1 of section at index 1. End updates.
Figure 7-2 illustrates what takes place after the animation block concludes. Figure 7-2
Section 0 0 1 2 Section 1 0 1 2 Section 2 0 1 2
0 1 Section 1 0 New 1 2 3
78
CHAPTER 8
A table view has an editing mode as well as its normal (selection) mode. When a table view goes into editing mode, it displays the editing and reordering controls associated with its rows. The reordering control allows the user to move a row to a different location in the table. As shown in Figure 8-1, the reordering control appears on the right side of the row. Figure 8-1 Reordering a row
When a table view enters editing mode and when users drag a reordering control, the table view sends a series of messages to its data source and delegate, but only if they implement these methods. These methods allow the data source and delegate to restrict whether and where a row can be moved as well to carry out the actual move operation. The following sections show you how to move rows around in a table view.
79
CHAPTER 8
Note: If a UIViewController object is managing the table view, it automatically receives a setEditing:animated: message when the Edit button is tapped. UITableViewController, a subclass of UIViewController, implements this method to update button state and invoke the table views version of the method. If you are using UIViewController to manage a table view, you need to implement the same behavior. When the table view receives setEditing:animated:, it sends the same message to the UITableViewCell object for each visible row. Then it sends a succession of messages to its data source and its delegate (if they implement the methods) as depicted in the diagram in Figure 8-2. Figure 8-2
Client
User presses Edit button setEditing:YES animated:YES tableView: canMoveRowAtIndexPath:
Data Source
tableView: moveRowAtIndexPath: toRowAtIndexPath:
When the table view receives the setEditing:animated: message, it resends the same message to the cell objects corresponding to its visible rows. After that, the sequence of messages is as follows: 1. The table view sends a tableView:canMoveRowAtIndexPath: message to its data source (if it implements the method). In this method the delegate may selectively exclude certain rows from showing the reordering control. The user drags a row by its reordering control up or down the table view. As the dragged row hovers over a part of the table view, the underlying row slides downward to show where the destination would be. Every time the dragged row is over a destination, the table view sends
tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: to its
2.
3.
delegate (if it implements the method). In this method the delegate may reject the current destination for the dragged row and specify an alternative one. 4. The table view sends tableView:moveRowAtIndexPath:toIndexPath: to its data source (if it implements the method). In this method the data source updates the data-model array that is the source of items for the table view, moving the item to a different location in the array.
80
CHAPTER 8
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.row == 0) // Don't move the first row return NO; return YES; }
When the user finishes dragging a row, it slides into its destination in the table view, which sends tableView:moveRowAtIndexPath:toIndexPath: to its data source. Listing 8-2 shows an implementation of this method. Note that it retains the data item fetched from the array (the item to be relocated) because the item is automatically released when it is removed from the array. Listing 8-2 Updating the data-model array for the relocated row
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { NSString *stringToMove = [[self.reorderingRows objectAtIndex:sourceIndexPath.row] retain]; [self.reorderingRows removeObjectAtIndex:sourceIndexPath.row]; [self.reorderingRows insertObject:stringToMove atIndex:destinationIndexPath.row]; [stringToMove release]; }
The delegate can also retarget the proposed destination for a move to another row by implementing the tableView:targetIndexPathForMoveFromRowAtIndexPath:toProposedIndexPath: method. The following example restricts rows to relocation in their own group and prevents moves to the last row of a group (which is reserved for the add-item placeholder). Listing 8-3 Retargeting the destination row of a move operation
- (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { NSDictionary *section = [data objectAtIndex:sourceIndexPath.section]; NSUInteger sectionCount = [[section valueForKey:@"content"] count]; if (sourceIndexPath.section != proposedDestinationIndexPath.section) { NSUInteger rowInSourceSection = (sourceIndexPath.section > proposedDestinationIndexPath.section) ? 0 : sectionCount - 1; return [NSIndexPath indexPathForRow:rowInSourceSection inSection:sourceIndexPath.section]; } else if (proposedDestinationIndexPath.row >= sectionCount) {
81
CHAPTER 8
return [NSIndexPath indexPathForRow:sectionCount - 1 inSection:sourceIndexPath.section]; } // Allow the proposed destination. return proposedDestinationIndexPath; }
82
REVISION HISTORY
This table describes the changes to Table View Programming Guide for iPhone OS. Date 2010-03-24 2009-08-19 Notes Made the introduction more informative. Explained how to set the background color of cells, emphasized purpose of tableView:willDisplayCell:forRowAtIndexPath:, and made minor corrections. Updated to describe 3.0 API, especially predefined cell styles and related properties. It also describes the use of nib files with table views and table-view cells and includes an updated chapter on view controllers and design patterns and strategies. Warned against calling setEditing:animated: when in tableView:commitEditingStyle:forRowAtIndexPath:. Updated illustration. Added section on batch insertions and deletions, added related classes to TOC frame, added guidelines on clearing selection, and made minor corrections. First version of this document.
2009-05-28
2008-10-15
2008-09-09
2008-06-25
83
2010-03-24 | 2010 Apple Inc. All Rights Reserved.
REVISION HISTORY
84
2010-03-24 | 2010 Apple Inc. All Rights Reserved.