Chapter 8

Introduction to Table Views

In the next chapter, we're going to build a hierarchical navigation-based application similar to the Mail application that ships on iOS devices. Our application will allow the user to drill down into nested lists of data and edit that data. But before we can build that application, you need to master the concept of table views. And that's the goal of this chapter.

Table views are the most common mechanism used to display lists of data to the user. They are highly configurable objects that can be made to look practically any way you want them to look. Mail uses table views to show lists of accounts, folders, and messages, but table views are not limited to just the display of textual data. Table views are also used in the YouTube, Settings, and iPod applications, even though these applications have very different appearances (see Figure 8–1).

images

Figure 8–1. Though they all look different, the Settings, iPod, and YouTube applications use table views to display their data.

Table View Basics

Tables display lists of data. Each item in a table's list is a row. iOS tables can have an unlimited number of rows, constrained only by the amount of available memory. iOS tables can be only one column wide.

Table Views and Table View Cells

A table view is the view object that displays a table's data and is an instance of the class UITableView. Each visible row of the table is implemented by the class UITableViewCell. So, a table view is the object that displays the visible part of a table, and a table view cell is responsible for displaying a single row of the table (see Figure 8–2).

images

Figure 8–2. Each table view is an instance of UITableView, and each visible row is an instance of UITableViewCell.

Table views are not responsible for storing your table's data. They store only enough data to draw the rows that are currently visible. Table views get their configuration data from an object that conforms to the UITableViewDelegate protocol and their row data from an object that conforms to the UITableViewDataSource protocol. You'll see how all this works when we get into our sample programs later in the chapter.

As mentioned, all tables are implemented as a single column. But the YouTube application, shown on the right side of Figure 8–1, does give the appearance of having at least two columns, perhaps even three if you count the icons. But no, each row in the table is represented by a single UITableViewCell. Each UITableViewCell object can be configured with an image, some text, and an optional accessory icon, which is a small icon on the right side (we'll cover accessory icons in detail in the next chapter).

You can put even more data in a cell if you need to by adding subviews to UITableViewCell, using one of two basic techniques: adding subviews programmatically when creating the cell, or loading them from a nib file. You can lay out the table view cell out in any way you like and include any subviews you desire. So, the single-column limitation is far less limiting than it probably sounds at first. If this is confusing, don't worry—we'll show you how to use both of these techniques in this chapter.

Grouped and Plain Tables

Table views come in two basic styles:

  • Grouped: Each group in a grouped table is a set of rows embedded in a rounded rectangle, as shown in the leftmost picture in Figure 8–3. Note that a grouped table can consist of a single group.
  • Plain: Plain is the default style. Any table that doesn't feature rounded rectangles is a plain table view. When an index is used, this style is also referred to as indexed.

If your data source provides the necessary information, the table view will let the user navigate your list using an index that is displayed down the right side. Figure 8–3 shows a grouped table, a plain table without an index, and a plain table with an index (an indexed table).

images

Figure 8–3. The same table view displayed as a grouped table (left), a plain table without an index (middle), and a plain table with an index, also called an indexed table (right)

Each division of your table is known to your data source as a section. In a grouped table, each group is a section. In an indexed table, each indexed grouping of data is a section. For example, in the indexed table shown in Figure 8–3, all the names beginning with A would be one section, those beginning with B another, and so on.

Sections have two primary purposes. In a grouped table, each section represents one group. In an indexed table, each section corresponds to one index entry. For example, if you wanted to display a list indexed alphabetically with an index entry for every letter, you would have 26 sections, each containing all the values that begin with a particular letter.

CAUTION: Even though it is technically possible to create a grouped table with an index, you should not do so. The iPhone Human Interface Guidelines specifically state that grouped tables should not provide indexes.

Implementing a Simple Table

Let's look at the simplest possible example of a table view to get a feel for how it works. In this example, we're just going to display a list of text values.

Create a new project in Xcode. For this chapter, we're going back to the Single View Application template, so select that one. Call your project Simple Table, enter BID as the Class Prefix, and set the Device Family to iPhone. Be sure the Use Storyboard and Include Unit Tests checkboxes are unchecked.

Designing the View

In the project navigator, expand the Simple Table project and the Simple Table folder. This is such a simple application that we're not going to need any outlets or actions. Go ahead and select BIDViewController.xib to edit the GUI. If the View window isn't visible in the layout area, single-click its icon in the dock to open it. Then look in the object library for a Table View (see Figure 8–4), and drag that over to the View window.

The table view should automatically size itself to the height and width of the view. This is exactly what we want. Table views are designed to fill the entire width of the screen and as much of the height as isn't taken up by your application's navigation bars, toolbars, and tab bars.

After dropping the table view onto the View window and fitting it just below the status bar, it should still be selected. If it's not, single-click the table view to select it. Then press ImagesImages6 to bring up the connections inspector. You'll notice that the first two available connections for the table view are the same as the first two for the picker view: dataSource and delegate. Drag from the circle next to each of those connections over to the File's Owner icon. By doing this, we are making our controller class both the data source and delegate for this table.

After setting the connections, save your nib file and get ready to dig into some UITableView code.

images

Figure 8–4. Dragging a table view from the library onto our main view. Notice that the table view automatically resizes to the full size of the view.

Writing the Controller

The next stop is our controller class's header file. Single-click BIDViewController.h, and add the following code:

#import <UIKit/UIKit.h>

@interface BIDViewController : UIViewController
       <UITableViewDelegate, UITableViewDataSource>

@property (strong, nonatomic) NSArray *listData;
@end

All we're doing here is conforming our class to the two protocols that are needed for it to act as the delegate and data source for the table view, and then declaring an array that will hold the data to be displayed.

Save your changes. Next, switch over to BIDViewController.m, and add the following code at the beginning of the file:

#import "BIDViewController.h"

@implementation BIDViewController
@synthesize listData;
.
.
.
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSArray *array = [[NSArray alloc] initWithObjects:@"Sleepy", @"Sneezy",
        @"Bashful", @"Happy", @"Doc", @"Grumpy", @"Dopey", @"Thorin",
        @"Dorin", @"Nori", @"Ori", @"Balin", @"Dwalin", @"Fili", @"Kili",
        @"Oin", @"Gloin", @"Bifur", @"Bofur", @"Bombur", nil];
    self.listData = array;
}
.
.
.

Now, add the following line of code to the existing viewDidUnload method:

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.listData = nil;
}

Finally, add the following code at the end of the file:

.
.
.
#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    return [self.listData count];
}

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

    static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
        SimpleTableIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]
           initWithStyle:UITableViewCellStyleDefault
           reuseIdentifier:SimpleTableIdentifier];
    }

    NSUInteger row = [indexPath row];
    cell.textLabel.text = [listData objectAtIndex:row];
    return cell;
}

@end

We added three methods to the controller. You should be comfortable with the first one, viewDidLoad, since we've done similar things in the past. We're simply creating an array of data to pass to the table. In a real application, this array would likely come from another source, such as a text file, property list, or URL.

If you scroll down to the end, you can see we added two data source methods. The first one, tableView:numberOfRowsInSection:, is used by the table to ask how many rows are in a particular section. As you might expect, the default number of sections is one, and this method will be called to get the number of rows in the one section that makes up the list. We just return the number of items in our array.

The next method probably requires a little explanation, so let's look at it more closely.

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

This method is called by the table view when it needs to draw one of its rows. Notice that the second argument to this method is an NSIndexPath instance. This is the mechanism that table views use to wrap the section and row into a single object. To get the row or the section out of an NSIndexPath, you just call either its row method or its section method, both of which return an int.

The first parameter, tableView, is a reference to the table doing the asking. This allows us to create classes that act as a data source for multiple tables.

Next, we declare a static string instance.

static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

This string will be used as a key to represent the type of our table cell. Our table will use only a single type of cell.

A table view can display only a few rows at a time on the iPhone's small screen, but the table itself can conceivably hold considerably more. Remember that each row in the table is represented by an instance of UITableViewCell, a subclass of UIView, which means each row can contain subviews. With a large table, this could represent a huge amount of overhead if the table were to try to keep one table view cell instance for every row in the table, regardless of whether that row was currently being displayed. Fortunately, tables don't work that way.

Instead, as table view cells scroll off the screen, they are placed into a queue of cells available to be reused. If the system runs low on memory, the table view will get rid of the cells in the queue. But as long as the system has some memory available for those cells, it will hold on to them in case you want to use them again.

Every time a table view cell rolls off the screen, there's a pretty good chance that another one just rolled onto the screen on the other side. If that new row can just reuse one of the cells that has already rolled off the screen, the system can avoid the overhead associated with constantly creating and releasing those views. To take advantage of this mechanism, we'll ask the table view to give us a previously used cell of the specified type. Note that we're making use of the NSString identifier we declared earlier. In effect, we're asking for a reusable cell of type SimpleTableIdentifier.

   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
       SimpleTableIdentifier];

Now, it's completely possible that the table view won't have any spare cells (when it's being initially populated, for example), so we check cell after the call to see whether it's nil. If it is, we manually create a new table view cell using that identifier string. At some point, we'll inevitably reuse one of the cells we create here, so we need to make sure that we create it using SimpleTableIdentifier.

   if (cell == nil) {
       cell = [[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:SimpleTableIdentifier];
   }

Curious about UITableViewCellStyleDefault? Hold that thought. We'll get to it when we look at the table view cell styles.

We now have a table view cell that we can return for the table view to use. So, all we need to do is place whatever information we want displayed in this cell. Displaying text in a row of a table is a very common task, so the table view cell provides a UILabel property called textLabel that we can set in order to display strings. That just requires getting the correct string from our listData array and using it to set the cell's textLabel.

To get the correct value, however, we need to know which row the table view is asking for. We get that information from the indexPath variable, like so:

NSUInteger row = [indexPath row];

We use the row number of the table to get the corresponding string from the array, assign it to the cell's textLabel.text property, and then return the cell.

   cell.textLabel.text = [listData objectAtIndex:row];
   return cell;
}

That wasn't so bad, was it? Compile and run your application, and you should see the array values displayed in a table view (see Figure 8–5).

images

Figure 8–5. The Simple Table application, in all its dwarven glory

Adding an Image

It would be nice if we could add an image to each row. Guess we would need to create a subclass of UITableViewCell or add subviews in order to do that, huh? Actually, no, not if you can live with the image being on the left side of each row. The default table view cell can handle that situation just fine. Let's check it out.

In the project archive, in the 08 - Simple Table folder, grab the file called star.png, and add it to your project's Simple Table folder. star.png is a small icon we prepared just for this project.

Next, let's get to the code. In the file BIDViewController.m, add the following code to the tableView:cellForRowAtIndexPath: method:

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

    static NSString *SimpleTableIdentifier = @" SimpleTableIdentifier ";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
                                SimpleTableIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:SimpleTableIdentifier];
    }

    UIImage *image = [UIImage imageNamed:@"star.png"];
    cell.imageView.image = image;

    NSUInteger row = [indexPath row];
    cell.textLabel.text = [listData objectAtIndex:row];

    return cell;
}
@end

Yep, that's it. Each cell has an imageView property. Each imageView has an image property, as well as a highlightedImage property. The image appears to the left of the cell's text and is replaced by the highlightedImage, if one is provided, when the cell is selected. You just set the cell's imageView.image property to whatever image you want to display.

If you compile and run your application now, you should get a list with a bunch of nice little star icons to the left of each row (see Figure 8–6). Of course, we could have included a different image for each row in the table. Or, with very little effort, we could have used one icon for all of Mr. Disney's dwarves and a different one for Mr. Tolkien's.

images

Figure 8–6. We used the cell's image property to add an image to each of the table view's cells.

If you like, make a copy of star.png, use your favorite graphics application to colorize it a bit, add it to the project, load it with imageNamed:, and use it to set imageView.highlightedImage. Now if you click a cell, your new image will be drawn. If you don't feel like coloring, use the star2.png icon we provided in the project archive.

NOTE: UIImage uses a caching mechanism based on the file name, so it won't load a new image property each time imageNamed: is called. Instead, it will use the already cached version.

Using Table View Cell Styles

The work you've done with the table view so far has made use of the default cell style shown in Figure 8–6, represented by the constant UITableViewCellStyleDefault. But the UITableViewCell class includes several other predefined cell styles that let you easily add a bit more variety to your table views. These cell styles make use of three different cell elements:

  • Image: If an image is part of the specified style, the image is displayed to the left of the cell's text.
  • Text label: This is the cell's primary text. In the style we used earlier, UITableViewCellStyleDefault, the text label is the only text shown in the cell.
  • Detail text label: This is the cell's secondary text, usually used as an explanatory note or label.

To see what these new style additions look like, add the following code to tableView:cellForRowAtIndexPath: in BIDViewController.m:

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

    static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
                             SimpleTableIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier: SimpleTableIdentifier];
    }

    UIImage *image = [UIImage imageNamed:@"star.png"];
    cell.imageView.image = image;

    NSUInteger row = [indexPath row];
    cell.textLabel.text = [listData objectAtIndex:row];

    if (row < 7)
        cell.detailTextLabel.text = @"Mr. Disney";
    else
        cell.detailTextLabel.text = @"Mr. Tolkien";


    return cell;
}

All we've done here is set the cell's detail text. We use the string @“Mr. Disney” for the first seven rows and @“Mr. Tolkien” for the rest. When you run this code, each cell will look just the same as it did before (see Figure 8–7). That's because we are using the style UITableViewCellStyleDefault, which does not make use of the detail text.

images

Figure 8–7. The default cell style shows the image and text label in a straight row.

Now, change UITableViewCellStyleDefault to UITableViewCellStyleSubtitle and run the app again. With the subtitle style, both text elements are shown, one below the other (see Figure 8–8).

images

Figure 8–8. The subtitle style shows the detail text in smaller, gray letters below the text label.

Change UITableViewCellStyleSubtitle to UITableViewCellStyleValue1, and then build and run. This style places the text label and detail text label on the same line on opposite sides of the cell (see Figure 8–9).

images

Figure 8–9. The style value 1 will place the text label on the left side in black letters and the detail text right-justified on the right side in blue letters.

Finally, change UITableViewCellStyleValue1 to UITableViewCellStyleValue2. This format is often used to display information along with a descriptive label. It doesn't show the cell's icon, but places the detail text label to the left of the text label (see Figure 8–10). In this layout, the detail text label acts as a label describing the type of data held in the text label.

images

Figure 8–10. The style value 2 does not display the image and places the detail text label in blue letters to the left of the text label.

Now that you've seen the cell styles that are available, go ahead and change back to using UITableViewCellStyleDefault before continuing. Later in this chapter, you'll see how to customize the appearance of your table. But before you decide to do that, make sure you consider the available styles to see whether one of them will suit your needs.

You may have noticed that we made our controller both the data source and delegate for this table view, but up to now, we haven't actually implemented any of the methods from UITableViewDelegate. Unlike picker views, simpler table views don't require the use of a delegate in order to do their thing. The data source provides all the data needed to draw the table. The purpose of the delegate is to configure the appearance of the table view and to handle certain user interactions. Let's take a look at a few of the configuration options now. We'll discuss a few more in the next chapter.

Setting the Indent Level

The delegate can be used to specify that some rows should be indented. In the file BIDViewController.m, add the following method to your code, just above the @end declaration:

#pragma mark -
#pragma mark Table Delegate Methods

- (NSInteger)tableView:(UITableView *)tableView
   indentationLevelForRowAtIndexPath:(NSIndexPath *)indexPath {
       NSUInteger row = [indexPath row];
    return row;
}

This method sets the indent level for each row to its row number, so row 0 will have an indent level of 0, row 1 will have an indent level of 1, and so on. An indent level is simply an integer that tells the table view to move that row a little to the right. The higher the number, the further to the right the row will be indented. You might use this technique, for example, to indicate that one row is subordinate to another row, as Mail does when representing subfolders.

When you run the application again, you can see that each row is now drawn a little farther to the right than the last one (see Figure 8–11).

images

Figure 8–11. Each row of the table is drawn with an indent level higher than the row before it.

Handling Row Selection

The table's delegate can use two methods to determine if the user has selected a particular row. One method is called before the row is selected and can be used to prevent the row from being selected, or even change which row gets selected. Let's implement that method and specify that the first row is not selectable. Add the following method to the end of BIDViewController.m, just before the @end declaration:

-(NSIndexPath *)tableView:(UITableView *)tableView
      willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
   NSUInteger row = [indexPath row];

   if (row == 0)
       return nil;

   return indexPath;
}

This method is passed indexPath, which represents the item that's about to be selected. Our code looks at which row is about to be selected. If the row is the first row, which is always index zero, then it returns nil, which indicates that no row should actually be selected. Otherwise, it returns indexPath, which is how we indicate that it's OK for the selection to proceed.

Before you compile and run, let's also implement the delegate method that is called after a row has been selected, which is typically where you'll actually handle the selection. This is where you take whatever action is appropriate when the user selects a row. In the next chapter, we'll use this method to handle the drill-downs, but in this chapter, we'll just put up an alert to show that the row was selected. Add the following method to the bottom of BIDViewController.m, just before the @end declaration again:

- (void)tableView:(UITableView *)tableView
       didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    NSUInteger row = [indexPath row];
    NSString *rowValue = [listData objectAtIndex:row];

    NSString *message = [[NSString alloc] initWithFormat:
        @"You selected %@", rowValue];
    UIAlertView *alert = [[UIAlertView alloc]
        initWithTitle:@"Row Selected!"
              message:message
              delegate:nil
    cancelButtonTitle:@"Yes I Did"
    otherButtonTitles:nil];
    [alert show];

    [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

Once you've added this method, compile and run the app, and take it for a spin. See whether you can select the first row (you shouldn't be able to), and then select one of the other rows. The selected row should be highlighted, and then your alert should pop up, telling you which row you selected, while the selected row fades in the background (see Figure 8–12).

images

Figure 8–12. In this example, the first row is not selectable, and an alert is displayed when any other row is selected. This was done using the delegate methods.

Note that you can also modify the index path before you pass it back, which would cause a different row and/or section to be selected. You won't do that very often, as you should have a very good reason for changing the user's selection. In the vast majority of cases, when you use this method, you will either return indexPath unmodified to allow the selection or return nil to or disallow it.

Changing the Font Size and Row Height

Let's say that we want to change the size of the font being used in the table view. In most situations, you shouldn't override the default font; it's what users expect to see. But sometimes there are valid reasons to change the font. Add the following line of code to your tableView:cellForRowAtIndexPath: method:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *SimpleTableIdentifier = @"SimpleTableIdentifier";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
                             SimpleTableIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier: SimpleTableIdentifier];
    }

    UIImage *image = [UIImage imageNamed:@"star.png"];
    cell.image = image;

    NSUInteger row = [indexPath row];
    cell.textLabel.text = [listData objectAtIndex:row];
    cell.textLabel.font = [UIFont boldSystemFontOfSize:50];

    if (row < 7)
        cell.detailTextLabel.text = @"Mr. Disney";
    else
        cell.detailTextLabel.text = @"Mr. Tolkein";
    return cell;
}

When you run the application now, the values in your list are drawn in a really large font size, but they don't exactly fit in the row (see Figure 8–13).

images

Figure 8–13. Look how nice and big! But, um, it would be even nicer if we could see everything.

Well, here comes the table view delegate to the rescue! The table view delegate can specify the height of the table view's rows. In fact, it can specify unique values for each row if you find that necessary. Go ahead and add this method to your controller class, just before @end:

- (CGFloat)tableView:(UITableView *)tableView
    heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 70;
}

We've just told the table view to set the row height for all rows to 70 pixels tall. Compile and run, and your table's rows should be much taller now (see Figure 8–14).

images

Figure 8–14. Changing the row size using the delegate

There are more tasks that the delegate handles, but most of the remaining ones come into play when you start working with hierarchical data, which we'll do in the next chapter. To learn more, use the documentation browser to explore the UITableViewDelegate protocol and see what other methods are available.

Customizing Table View Cells

You can do a lot with table views right out of the box, but often, you will want to format the data for each row in ways that simply aren't supported by UITableViewCell directly. In those cases, there are two basic approaches: one that involves adding subviews to UITableViewCell programmatically when creating the cell, and a second that involves loading a set of subviews from a nib file. Let's look at both techniques.

Adding Subviews to the Table View Cell

To show how to use custom cells, we're going to create a new application with another table view. In each row, we'll display two lines of information along with two labels (see Figure 8–15). Our application will display the name and color of a series of potentially familiar computer models, and we'll show both of those pieces of information in the same table cell by adding subviews to the table view cell.

images

Figure 8–15. Adding subviews to the table view cell can give you multiline rows.

Create a new Xcode project using the Single View Application template. Name the project Cells, and use the same settings as your last project. Click BIDViewController.xib to edit the nib file in Interface Builder.

Add a Table View to the main view, use the connections inspector to set its delegate and data source to File's Owner as we did for the Simple Table application, and then save the nib.

Creating a UITableViewCell Subclass

Up until this point, the standard table view cells we've been using have taken care of all the details of cell layout for us. Our controller code has been kept clear of the messy details about where to place labels and images, and has been able to just pass off the display values to the cell. This keeps presentation logic out of the controller, and that's a really good design to stick to. For this project, we're going to make a new cell subclass of our own that takes care of the details for the new layout, which will keep our controller as simple as possible.

Adding New Cells

Select the Cells folder in the project navigator, and press ImagesN to create a new file. In the assistant that pops up, choose Objective-C class from the Cocoa Touch section, and click Next. On the following screen, enter BIDNameAndColorCell as the name of the new class, select UITableViewCell in the Subclass of popup list, and click Next again. On the final screen, select the Cells folder that already contains your other source code, make sure Cells is chosen both in the Group and Target controls at the bottom, and click Create.

Select BIDNameAndColorCell.h, and add the following code:

#import <UIKit/UIKit.h>
@interface BIDNameAndColorCell : UITableViewCell

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *color;


@end

Here, we've defined two properties that our controller will use to pass values to each cell. Note that instead of declaring the NSString properties with strong semantics, we're using copy. Doing so with NSString values is always a good idea, because there's a risk that the string value passed in to a property setter may actually be an NSMutableString, which the sender can modify later on, leading to problems. Copying each string that's passed in to a property gives us a stable, unchangeable snapshot of what the string contains at the moment the setter is called.

That's all we need to expose in the header, so let's move on to BIDNameAndColorCell.m. Add the following code at the top of the file:

#import "BIDNameAndColorCell.h"

#define kNameValueTag    1
#define kColorValueTag   2


@implementation BIDNameAndColorCell

@synthesize name;
@synthesize color;

.
.
.

Notice that we've defined two constants. We're going to use these to assign tags to some of the subviews that we'll be adding to the table view cell. We'll add four subviews to the cell, and two of those need to be changed for every row. In order to do that, we need some mechanism that will allow us to retrieve the two fields from the cell when we go to update that cell with a particular row's data. If we set unique tag values for each label that we'll use again, we'll be able to retrieve them from the table view cell and set their values. We also declared the name and color properties, which the controller will use to set the values that should appear in the cell.

Now, edit the existing initWithStyle:reuseIdentifier: method to create the views that we'll need to display.

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString
*)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        // Initialization code
        CGRect nameLabelRect = CGRectMake(0, 5, 70, 15);
        UILabel *nameLabel = [[UILabel alloc] initWithFrame:nameLabelRect];
        nameLabel.textAlignment = UITextAlignmentRight;
        nameLabel.text = @"Name:";
        nameLabel.font = [UIFont boldSystemFontOfSize:12];
        [self.contentView addSubview: nameLabel];

        CGRect colorLabelRect = CGRectMake(0, 26, 70, 15);
        UILabel *colorLabel = [[UILabel alloc] initWithFrame:colorLabelRect];
        colorLabel.textAlignment = UITextAlignmentRight;
        colorLabel.text = @"Color:";
        colorLabel.font = [UIFont boldSystemFontOfSize:12];
        [self.contentView addSubview: colorLabel];

        CGRect nameValueRect = CGRectMake(80, 5, 200, 15);
        UILabel *nameValue = [[UILabel alloc] initWithFrame:
                              nameValueRect];
        nameValue.tag = kNameValueTag;
        [self.contentView addSubview:nameValue];

        CGRect colorValueRect = CGRectMake(80, 25, 200, 15);
        UILabel *colorValue = [[UILabel alloc] initWithFrame:
                               colorValueRect];
        colorValue.tag = kColorValueTag;
        [self.contentView addSubview:colorValue];

    }
    return self;
}

That should be pretty straightforward. We create four UILabels and add them to the table view cell. The table view cell already has a UIView subview called contentView, which it uses to group all of its subviews, much the way we grouped those two switches inside a UIView back in Chapter 4. As a result, we don't add the labels as subviews directly to the table view cell, but rather to its contentView.

[self.contentView addSubview:colorValue];

Two of these labels contain static text. The label nameLabel contains the text Name:, and the label colorLabel contains the text Color:. Those are just labels that we won't change. But we'll use the other two labels to display our row-specific data. Remember that we need some way of retrieving these fields later on, so we assign values to both of them. For example, we assign the constant kNameValueTag to nameValue's tag field.

   nameValue.tag = kNameValueTag;

Now, to put the finishing touches on the BIDNameAndColorCell class, add these two setter methods just before the @end:

- (void)setName:(NSString *)n {
    if (![n isEqualToString:name]) {
        name = [n copy];
        UILabel *nameLabel = (UILabel *)[self.contentView viewWithTag:
                                    kNameValueTag];
        nameLabel.text = name;
    }
}

- (void)setColor:(NSString *)c {
    if (![c isEqualToString:color]) {
        color = [c copy];
        UILabel *colorLabel = (UILabel *)[self.contentView viewWithTag:
                                    kColorValueTag];
        colorLabel.text = color;
    }
}

You already know that using @synthesize, as we did at the top of the file, creates getter and setter methods for each property. Yet, here we're defining our own setters for both name and color! As it turns out, this is just fine. Any time a class defines its own getters or setters, those will be used instead of the default methods that @synthesize provides. In this class, we're using the default, synthesized getters, but defining our own setters, so that whenever we are passed new values for the name or color properties, we update the labels we created earlier.

Implementing the Controller's Code

Now, let's set up the simple controller to display values in our nice new cells. Start off by selecting BIDViewController.h, where you need to add the following code:

#import <UIKit/UIKit.h>

@interface BIDViewController : UIViewController
    <UITableViewDataSource, UITableViewDelegate>

@property (strong, nonatomic) NSArray *computers;

@end

In our controller, we need to set up some data to use, and then implement the table data source methods to feed that data to the table. Switch to BIDViewController.m, and add the following code at the beginning of the file:

#import "BIDViewController.h"
#import "BIDNameAndColorCell.h"

@implementation ViewController
@synthesize computers;
.
.
.
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    NSDictionary *row1 = [[NSDictionary alloc] initWithObjectsAndKeys:
                      @"MacBook", @"Name", @"White", @"Color", nil];
    NSDictionary *row2 = [[NSDictionary alloc] initWithObjectsAndKeys:
                      @"MacBook Pro", @"Name", @"Silver", @"Color", nil];
    NSDictionary *row3 = [[NSDictionary alloc] initWithObjectsAndKeys:
                      @"iMac", @"Name", @"Silver", @"Color", nil];
    NSDictionary *row4 = [[NSDictionary alloc] initWithObjectsAndKeys:
                      @"Mac Mini", @"Name", @"Silver", @"Color", nil];
    NSDictionary *row5 = [[NSDictionary alloc] initWithObjectsAndKeys:
                      @"Mac Pro", @"Name", @"Silver", @"Color", nil];

    self.computers = [[NSArray alloc] initWithObjects:row1, row2,
                      row3, row4, row5, nil];

}
.
.
.

Of course, we need to be good memory citizens, so make the following changes to the existing viewDidUnload method:

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.computers = nil;
}

Then add this code at the end of the file, above the @end declaration:

.
.
.
#pragma mark -
#pragma mark Table Data Source Methods
- (NSInteger)tableView:(UITableView *)tableView
    numberOfRowsInSection:(NSInteger)section {
    return [self.computers count];
}

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

    static NSString *CellTableIdentifier = @"CellTableIdentifier";

    BIDNameAndColorCell *cell = [tableView dequeueReusableCellWithIdentifier:
                             CellTableIdentifier];
    if (cell == nil) {
        cell = [[BIDNameAndColorCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellTableIdentifier];
    }

    NSUInteger row = [indexPath row];
    NSDictionary *rowData = [self.computers objectAtIndex:row];

    cell.name = [rowData objectForKey:@"Name"];
    cell.color = [rowData objectForKey:@"Color"];

    return cell;
}


@end

This version of viewDidLoad creates a series of dictionaries. Each dictionary contains the name and color information for one row in the table. The name for that row is held in the dictionary under the key Name, and the color is held under the key Color. We stick all the dictionaries into a single array, which is our data for this table.

NOTE: Remember when Macs came in different colors, like beige, platinum, black, and white? And that's not to mention the original iMac and MacBook series, with their beautiful rainbow hues assortment. Now there's just one color: silver. Harrumph.

Let's focus on tableView:cellForRowWithIndexPath:, since that's where we're really getting into some new stuff. The first two lines of code are nearly identical to our earlier versions. We create an identifier and ask the table to dequeue a table view cell if it has one. The only difference here is that we declare the cell variable to be an instance of our BIDNameAndColorCell class instead of the standard UITableViewCell. That's so we can access the properties that we added specifically to our table view cell subclass.

If the table doesn't have any cells available for reuse, we need to create a new cell. This is essentially the same technique as before, except that here we're also using our custom class instead of UITableViewCell. We specify the default style, although the style actually doesn't matter, because we'll be adding our own subviews to display our data rather than using the provided ones.

   cell = [[BIDNameAndColorCell alloc]
       initWithStyle:UITableViewCellStyleDefault
       reuseIdentifier:CellTableIdentifier];

Once we're finished creating our new cell, we use the indexPath argument that was passed in to determine which row the table is requesting a cell for, and then use that row value to grab the correct dictionary for the requested row. Remember that the dictionary has two key/value pairs: one with name and another with color.

    NSUInteger row = [indexPath row];
    NSDictionary *rowData = [self.computers objectAtIndex:row];

Now, all that's left to do is populate the cell with data from the chosen row, using the properties we defined in our subclass.

    cell.name = [rowData objectForKey:@"Name"];
    cell.color = [rowData objectForKey:@"Color"];

Compile and run your application. You should see a table of rows, each with two lines of data, as shown earlier in Figure 8–15.

Being able to add views to a table view cell provides a lot more flexibility than using the standard table view cell alone, but it can get a little tedious creating, positioning, and adding all the subviews programmatically. Gosh, it sure would be nice if we could design the table view cell graphically, using Xcode's nib editor. Well, we're in luck. As we mentioned earlier, you can use Interface Builder to design your table view cells, and then simply load the views from the nib file when you create a new cell.

Loading a UITableViewCell from a Nib

We're going to re-create that same two-line interface we just built in code using the visual layout capabilities that Xcode provides in Interface Builder. To do this, we'll create a new nib file that will contain the table view cell and lay out its views using Interface Builder. Then, when we need a table view cell to represent a row, instead of creating a standard table view cell, we'll just load the nib file and use the properties we already defined in our cell class to set the name and color. Besides making use of Interface Builder's visual layout, we'll also simplify our code in a few other places.

First, we'll make a few changes to the BIDNameAndColorCell class. Since we're going to wire things up in the nib editor, we'll add outlets to point out the labels that need to be accessed. Add these lines to the @interface declaration in BIDNameAndColorCell.h:

@interface BIDNameAndColorCell : UITableViewCell

@property (copy, nonatomic) NSString *name;
@property (copy, nonatomic) NSString *color;

@property (strong, nonatomic) IBOutlet UILabel *nameLabel;
@property (strong, nonatomic) IBOutlet UILabel *colorLabel;

@end

Now that we have these outlets, we don't need the tags anymore! Switch over to BIDNameAndColorCell.m, delete the tag definitions, and add method synthesis for our two new outlets:

#import "BIDNameAndColorCell.h"

#define kNameValueTag    1
#define kColorValueTag   2

@implementation BIDNameAndColorCell

@synthesize name;
@synthesize color;
@synthesize nameLabel;
@synthesize colorLabel;

Having those outlets available also means that both of our setters can be simplified by removing a couple of lines:

- (void)setName:(NSString *)n {
    if (![n isEqualToString:name]) {
        name = [n copy];
        UILabel *nameLabel = (UILabel *)[self.contentView viewWithTag:
                                         kNameValueTag];

        nameLabel.text = name;
    }
}

- (void)setColor:(NSString *)c {
    if (![c isEqualToString:color]) {
        color = [c copy];
        UILabel *colorLabel = (UILabel *)[self.contentView viewWithTag:
                                          kColorValueTag];

        colorLabel.text = color;
    }
}

And, last but not least, remember that setup we did in initWithStyle:reuseIdentifier:, where we created our labels? All that can go. In fact, you should just delete the entire method, since all that setup will now be done in Interface Builder.

After all that, you're left with a cell class that's even smaller and cleaner than before. Its only real function now is to shuffle data to the labels. Now we need to re-create the labels in Interface Builder.

Right-click the Cells folder in Xcode and select New File. . . from the contextual menu. In the left pane of the new file assistant, click User Interface (making sure to pick it in the iOS section, rather than the Mac OS X section). From the upper-right pane, select Empty, and then click Next. On the following screen, leave the Device Family popup set to iPhone and click Next once again. When prompted for a name, type BIDNameAndColorCell.xib. Make sure that the main project directory is selected in the file browser and that the Cells group is selected in the Group popup.

Designing the Table View Cell in Interface Builder

Next, select BIDNameAndColorCell.xib in the project navigator to open the file for editing. There are only two icons in this nib's dock: File's Owner and First Responder. Look in the library for a Table View Cell (see Figure 8–16), and drag one of those over to the GUI layout area.

images

Figure 8–16. We dragged a table view cell from the library into the nib editor's GUI layout area.

Make sure the table view cell is selected, press ImagesImages5 to bring up the size inspector, and in the View section, change the cell's height from 44 to 65. That will give us a little more room to play with.

Next, press ImagesImages4 to go to the attributes inspector (see Figure 8–17). One of the first fields you'll see there is Identifier. That's the reuse identifier that we've been using in our code. If this does not ring a bell, scan back through the chapter and look for CellTableIdentifier. Set the Identifier value to CellTableIdentifier.

images

Figure 8–17. The attributes inspector for a table view cell

The idea here is that when we retrieve a cell for reuse, perhaps because of scrolling a new cell into view, we want to make sure we get the correct cell type. When this particular cell is instantiated from the nib file, its reuse identifier instance variable will be prepopulated with the NSString you entered in the Identifier field of the attributes inspector—CellTableIdentifier in this case.

Imagine a scenario where you created a table with a header and then a series of “middle” cells. If you scroll a middle cell into view, it's important that you retrieve a middle cell to reuse and not a header cell. The Identifier field lets you tag the cells appropriately.

Our next step is to edit our table cell's content view. Go to the library, drag out four Label controls, and place them in the content view, using Figure 8–18 as a guide. The labels will be too close to the top and bottom for those guidelines to be of much help, but the left guideline and the alignment guidelines should serve their purpose. Note that you can drag out one label, and then option-drag to create copies, if that approach is easier for you.

images

Figure 8–18. The table view cell's content view, with four labels dragged in

Next, double-click the upper-left label and change it to Name:. Then change the lower-left label to Color:.

Now, select both the Name: and Color: labels, and press the small T button in the attribute inspector's Font field. This will open a small panel containing a Font popup button. Click that, and choose System Bold as the typeface. If needed, select the two unchanged label fields on the right and drag them a little more to the right to give the design a bit of breathing room.

Finally, resize the two right-side labels so they stretch all the way to the right guideline. Figure 8–19 should give you a sense of our final cell content view.

images

Figure 8–19. The table view cell's content view with the left label names changed and set to bold, and with the right labels slightly moved and resized

Now, we need to let Interface Builder know that this table view cell isn't just a normal cell, but rather our special subclass. Otherwise, we wouldn't be able to connect our outlets to the relevant labels. Select the table view cell, bring up the identity inspector by pressing ImagesImages3, and choose BIDNameAndColorCell from the Class control.

Next, switch to the connections inspector (ImagesImages6), where you'll see the colorLabel and nameLabel outlets. Drag each of them to their corresponding label in the GUI.

Using the New Table View Cell

To use the cell we designed, we just need to make a few pretty simple changes to the tableView:cellForRowAtIndexPath: method in BIDViewController.m. We're going to add a bit and take a bit away.

-(UITableViewCell *)tableView:(UITableView *)tableView
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *CellTableIdentifier = @"CellTableIdentifier";
    static BOOL nibsRegistered = NO;
    if (!nibsRegistered) {
        UINib *nib = [UINib nibWithNibName:@"BIDNameAndColorCell" bundle:nil];

        [tableView registerNib:nib forCellReuseIdentifier:CellTableIdentifier];
        nibsRegistered = YES;
    }


    BIDNameAndColorCell *cell = [tableView dequeueReusableCellWithIdentifier:
                             CellTableIdentifier];
if (cell == nil) {
        cell = [[BIDNameAndColorCell alloc]
                 initWithStyle:UITableViewCellStyleDefault
                 reuseIdentifier:CellTableIdentifier];
    }


    NSUInteger row = [indexPath row];
    NSDictionary *rowData = [self.computers objectAtIndex:row];

    cell.name = [rowData objectForKey:@"Name"];
    cell.color = [rowData objectForKey:@"Color"];

    return cell;
}

The first change you see here is the addition of a new static BOOL variable. This variable maintains its state across invocations of this method, and it is initialized to NO only the first time this method is called. This lets us insert a few lines that will be called only once (the first time this method is called), in order to register our nib with the table view. What does this mean?

Starting in iOS 5, a table view can keep track of which nib files are meant to be associated with particular reuse identifiers. UITableView's dequeueReusableCellWithIdentifier: method is now so smart that, even if there are no available cells, it can use this nib registry to load a new cell from a nib file. That means that as long as we've registered all the reuse identifiers we're going to use for a table view, its dequeueReusableCellWithIdentifier: method will always return a cell, and it never returns nil. Therefore, we can remove the lines that check for a nil cell value, since that will never happen.

There's one other addition we need to make. We already changed the height of our table view cell from the default value in CustomCell.xib, but that's not quite enough. We also need to inform the table view of that fact; otherwise, it won't leave enough space for the cell to display properly. The simplest way to do this is by adding a table view delegate method that lets us specify the value. Add the following new method to the bottom of the class definition in BIDViewController.m:

- (CGFloat)tableView:(UITableView *)tableView
        heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 65.0; // Same number we used in Interface Builder
}

That's it. Build and run. Now your two-line table cells are based on your mad Interface Builder design skillz.

So, now that you've seen a couple of approaches, what do you think? Many people who delve into iOS development are somewhat confused at first by the focus on Interface Builder, but as you've seen, it has a lot going for it. Besides having the obvious appeal of letting you visually design your GUI, this approach promotes the proper use of nib files, which helps you stick to the MVC architecture pattern. Also, you can make your application code simpler, more modular, and just plain easier to write. As our good buddy Mark Dalrymple says, “No code is the best code!”

Grouped and Indexed Sections

Our next project will explore another fundamental aspect of tables. We're still going to use a single table view—no hierarchies yet—but we'll divide data into sections. Create a new Xcode project using the Single View Application template again, this time calling it Sections.

Building the View

Open the Sections folders, andclick BIDViewController.xib to edit the file. Drop a table view onto the View window, as we did before. Then press ImagesImages6, and connect the dataSource and delegate connections to the File's Owner icon.

Next, make sure the table view is selected, and press ImagesImages4 to bring up the attributes inspector. Change the table view's Style from Plain to Grouped (see Figure 8–20). You should see the change reflected in the sample table shown in the table view. Save your nib, and move along. (We discussed the difference between indexed and grouped styles at the beginning of the chapter.)

images

Figure 8–20. The attributes inspector for the table view, showing the Style popup with Grouped selected

Importing the Data

This project needs a fair amount of data to do its thing. To save you a few hours worth of typing, we've provided another property list for your tabling pleasure. Grab the file named sortednames.plist from the 08 Sections/Sections subfolder in this book's project archive and add it to your project's Sections folder.

Once sortednames.plist is added to your project, single-click it just to get a sense of what it looks like (see Figure 8–21). It's a property list that contains a dictionary, with one entry for each letter of the alphabet. Underneath each letter is a list of names that start with that letter.

images

Figure 8–21. The sortednames.plist property list file. We opened the letter Z to give you a sense of one of the dictionaries.

We'll use the data from this property list to feed the table view, creating a section for each letter.

Implementing the Controller

Single-click the BIDViewController.h file, and add both an NSDictionary and an NSArray instance variable and corresponding property declarations. The dictionary will hold all of our data. The array will hold the sections sorted in alphabetical order. We also need to make the class conform to the UITableViewDataSource and UITableViewDelegate protocols.

#import <UIKit/UIKit.h>

@interface BIDViewController : UIViewController
    <UITableViewDataSource, UITableViewDelegate>

@property (strong, nonatomic) NSDictionary *names;
@property (strong, nonatomic) NSArray *keys;

@end

Now, switch over to BIDViewController.m, and add the following code to the beginning of that file:

#import "BIDViewController.h"

@implementation BIDViewController
@synthesize names;
@synthesize keys;

.
.
.
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString *path = [[NSBundle mainBundle] pathForResource:@"sortednames"
                                                     ofType:@"plist"];
    NSDictionary *dict = [[NSDictionary alloc]
                          initWithContentsOfFile:path];
    self.names = dict;

    NSArray *array = [[names allKeys] sortedArrayUsingSelector:
                      @selector(compare:)];
    self.keys = array;

}
.
.
.

Insert the following lines of code in the existing viewDidUnload method:

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.names = nil;
    self.keys = nil;

}

Now, add the following code at the end of the file, just above the @end declaration:

.
.
.
#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [keys count];
}

- (NSInteger)tableView:(UITableView *)tableView
        numberOfRowsInSection:(NSInteger)section {
    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];
    return [nameSection count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView
    cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSUInteger section = [indexPath section];
    NSUInteger row = [indexPath row];

    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];

    static NSString *SectionsTableIdentifier = @"SectionsTableIdentifier";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:
        SectionsTableIdentifier];
    if (cell == nil) {
        cell = [[UITableViewCell alloc]
            initWithStyle:UITableViewCellStyleDefault
            reuseIdentifier:SectionsTableIdentifier];
    }

    cell.textLabel.text = [nameSection objectAtIndex:row];
    return cell;
}

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
    NSString *key = [keys objectAtIndex:section];
    return key;
}


@end

Most of this isn't too different from what you've seen before. In the viewDidLoad method, we created an NSDictionary instance from the property list we added to our project and assigned it to names. After that, we grabbed all the keys from that dictionary and sorted them to give us an ordered NSArray with all the key values in the dictionary in alphabetical order. Remember that the NSDictionary uses the letters of the alphabet as its keys, so this array will have 26 letters, in order from A to Z, and we'll use that array to help us keep track of the sections.

Scroll down to the data source methods. The first one we added to our class specifies the number of sections. We didn't implement this method in the earlier example, because we were happy with the default setting of 1. This time, we're telling the table view that we have one section for each key in our dictionary.

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [keys count];
}

The next method calculates the number of rows in a specific section. In the previous example, we had only one section, so we just returned the number of rows in our array. This time, we need to break it down by section. We can do this by retrieving the array that corresponds to the section in question and returning the count from that array.

- (NSInteger)tableView:(UITableView *)tableView
        numberOfRowsInSection:(NSInteger)section {
    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];
    return [nameSection count];
}

In our tableView:cellForRowAtIndexPath: method, we need to extract both the section and row from the index path, and use those to determine which value to use. The section will tell us which array to pull out of the names dictionary, and then we can use the row to figure out which value from that array to use. Everything else in that method is basically the same as the version in the Simple Table application we built earlier in the chapter.

The method tableView:titleForHeaderInSection allows you to specify an optional header value for each section, and we simply return the letter for this group.

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
    NSString *key = [keys objectAtIndex:section];
    return key;
}

Compile and run the project, and revel in its grooviness. Remember that we changed the table's Style to Grouped, so we ended up with a grouped table with 26 sections, which should look like Figure 8–22.

As a contrast, let's change our table view back to the plain style and see what a plain table view with multiple sections looks like. Select BIDViewController.xib to edit the file in Interface Builder again. Select the table view, and use the attributes inspector to switch the view to Plain. Save the project, and then build and run it—same data, different grooviness (see Figure 8–23).

images

Figure 8–22. A grouped table with multiple sections

images

Figure 8–23. A plain table with sections and no index

Adding an Index

One problem with our current table is the sheer number of rows. There are 2,000 names in this list. Your finger will get awfully tired looking for Zachariah or Zayne, not to mention Zoie.

One solution to this problem is to add an index down the right side of the table view. Now that we've set our table view style back to Plain, that's relatively easy to do. Add the following method to the bottom of BIDViewController.m, just above the @end:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return keys;
}

Yep, that's it. In this method, the delegate is asking for an array of the values to display in the index. You must have more than one section in your table view to use the index, and the entries in this array must correspond to those sections. The returned array must have the same number of entries as you have sections, and the values must correspond to the appropriate section. In other words, the first item in this array will take the user to the first section, which is section 0.

Compile and run the app again, and you'll have yourself a nice index (see Figure 8–24).

images

Figure 8–24. The table view with an index

Implementing a Search Bar

The index is helpful, but even so, we still have a whole lot of names here. If we want to see whether the name Arabella is in the list, for example, we'll need to scroll for a while even after using the index. It would be nice if we could let the user pare down the list by specifying a search term, wouldn't it? That would be darn user-friendly. Well, it's a bit of extra work, but it's not too bad. We're going to implement a standard iOS search bar, like the one shown in Figure 8–25.

images

Figure 8–25. The application with a search bar added to the table

Rethinking the Design

Before we set about adding a search bar, we need to put some thought into our approach. Currently, we have a dictionary that holds a series of arrays, one for each letter of the alphabet. The dictionary is immutable, which means we can't add or delete values from it, and so are the arrays that it holds. We also need to retain the ability to get back to the original dataset when the user hits cancel or erases the search term.

The solution is to create two dictionaries: an immutable dictionary to hold the full dataset and a mutable copy from which we can remove rows. The delegate and data sources will read from the mutable dictionary, and when the search criteria change or the search is canceled, we can refresh the mutable dictionary from the immutable one. Sounds like a plan. Let's do it.

CAUTION: This next project is a bit advanced and may cause a distinct burning sensation if taken too quickly. If some of these concepts give you a headache, retrieve your copy of Learn Objective-C on the Mac by Mark Dalrymple and Scott Knaster (Apress, 2009) and review the bits about categories and mutability.

A Deep Mutable Copy

To use our new approach, there's one problem we'll need to solve. NSDictionary conforms to the NSMutableCopying protocol, which returns an NSMutableDictionary, but that method creates what's called a shallow copy. This means that when you call the mutableCopy method, it will create a new NSMutableDictionary object that has all the objects that the original dictionary had. They won't be copies; they will be the same actual objects. This would be fine if, say, we were dealing with a dictionary storing strings, because removing a value from the copy wouldn't do anything to the original. Since we have a dictionary full of arrays, however, if we were to remove objects from the arrays in the copy, we would also be removing them from the arrays in the original, because both the copies and the original point to the same objects. In this particular case, the original arrays are immutable, so you couldn't actually remove objects from them anyway, but our intention is to illustrate the point.

In order to deal with this properly, we need to be able to make a deep mutable copy of a dictionary full of arrays. That's not too hard to do, but where should we put this functionality?

If you said, “in a category,“ then great, now you're thinking with portals! If you didn't, don't worry, it takes a while to get used to this language. Categories, in case you've forgotten, allow you to add more methods to existing objects without subclassing them. Categories are frequently overlooked by folks new to Objective-C, because they're a feature most other languages don't have.

With categories, we can add a method to NSDictionary to do a deep copy, returning an NSMutableDictionary with the same data but not containing the same actual objects.

NOTE: Before you move on to this next series of steps, consider making a backup copy of your project. This way, you'll make sure you have a working version to go back to if things go south with this next set of changes.

In your project window, select the Sections folder, and press ImagesN to create a new file. When the new file assistant comes up, select Cocoa Touch from the very top of the iOS section. In the right-hand panel, select Objective-C category, since that's just what we want to create, and click Next. On the following screen, name your protocol MutableDeepCopy, and enter NSDictionary in the Category on field. Then click Next once again. On the final screen, make sure Sections is selected in the file browser, Group popup, and Target control.

Put the following code in NSDictionary+MutableDeepCopy.h:

#import <Foundation/Foundation.h>

@interface NSDictionary (MutableDeepCopy)
- (NSMutableDictionary *)mutableDeepCopy;
@end

Flip over to NSDictionary+MutableDeepCopy.m, and add the implementation:

#import "NSDictionary+MutableDeepCopy.h"

@implementation NSDictionary (MutableDeepCopy)

- (NSMutableDictionary *)mutableDeepCopy {
    NSMutableDictionary *returnDict = [[NSMutableDictionary alloc]
        initWithCapacity:[self count]];
    NSArray *keys = [self allKeys];
    for (id key in keys) {
        id oneValue = [self valueForKey:key];
        id oneCopy = nil;

        if ([oneValue respondsToSelector:@selector(mutableDeepCopy)])
            oneCopy = [oneValue mutableDeepCopy];
        else if ([oneValue respondsToSelector:@selector(mutableCopy)])
            oneCopy = [oneValue mutableCopy];
        if (oneCopy == nil)
            oneCopy = [oneValue copy];
        [returnDict setValue:oneCopy forKey:key];
    }
    return returnDict;

}

@end

This method creates a new mutable dictionary and then loops through all the keys of the original dictionary, making mutable copies of each array it encounters. Since this method will behave just as if it were part of NSDictionary, any reference to self is a reference to the dictionary on which this method is being called. The method first attempts to make a deep mutable copy, and if the object doesn't respond to the mutableDeepCopy message, it tries to make a mutable copy. If the object doesn't respond to the mutableCopy message, it falls back on making a regular copy to ensure that all the objects contained in the dictionary are copied. By doing it this way, if we were to have a dictionary that contained dictionaries (or other objects that supported deep mutable copies), the contained ones would also get deep-copied.

For a few of you, this might be the first time you've seen this syntax in Objective-C:

for (id key in keys)

Objective-C 2.0 introduced a feature called fast enumeration. Fast enumeration is a language-level replacement for NSEnumerator. It allows you to quickly iterate through a collection, such as an NSArray, without the hassle of creating additional objects or loop variables.

All of the delivered Cocoa collection classes—including NSDictionary, NSArray, and NSSet—support fast enumeration, and you should use this syntax any time you need to iterate over a collection. It will ensure that you get the most efficient loop possible.

If we include the NSDictionary+MutableDeepCopy.h header file in one of our other classes, we'll be able to call mutableDeepCopy on any NSDictionary object we like. Let's take advantage of that now.

Updating the Controller Header File

Next, we need to add some outlets to our controller class header file. We'll add an outlet for the table view. Up until now, we haven't needed a pointer to the table view outside the data source methods. But for our search bar implementation, we need one to tell the table to reload itself based on the result of the search. We're also going to add an outlet to a search bar, which is a control used for, well, searching.

In addition to those two outlets, we'll add another dictionary. The existing dictionary and array are both immutable objects, and we need to change both of them to the corresponding mutable version, so the NSArray becomes an NSMutableArray and the NSDictionary becomes an NSMutableDictionary.

We won't need any new action methods in our controller, but we will use a couple of new methods. For now, just declare them, and we'll talk about them in detail after you enter the code.

We also must conform our class to the UISearchBarDelegate protocol. We'll need to become the search bar's delegate in addition to being the table view's delegate.

Make the following changes to BIDViewController.h:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
<UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate>

@property (strong, nonatomic) NSDictionary *names;
@property (strong, nonatomic) NSArray *keys;

@property (strong, nonatomic) IBOutlet UITableView *table;
@property (strong, nonatomic) IBOutlet UISearchBar *search;
@property (strong, nonatomic) NSDictionary *allNames;
@property (strong, nonatomic) NSMutableDictionary *names;
@property (strong, nonatomic) NSMutableArray *keys;
- (void)resetSearch;
- (void)handleSearchForTerm:(NSString *)searchTerm;

@end

Here's what we just did:

  • The outlet table will point to our table view.
  • The outlet search will point to the search bar.
  • The dictionary allNames will hold the full dataset.
  • The dictionary names will hold the dataset that matches the current search criteria.
  • keys will hold the index values and section names.

If you're clear on everything, let's forge ahead and modify our view.

Modifying the View

Every table view allows for the possibility of placing a view at the top end of the table view, above any content. That header view will scroll with the rest of the content. A perfect example of this is the UISearchBar. If you drag a SearchBar from the library and place it just above the table view, Interface Builder will size the search bar so it fits at the top of the table view and scrolls with the table view.

Unfortunately, the UISearchBar does not play well with the right-hand indexing feature. If you take a look at Figure 8–26, you'll see why. Notice that the index overlaps the search bar, covering a bit of the Cancel button.

images

Figure 8–26. In the current version of our application, the search bar's Cancel button is overlapped by the index.

Fortunately, there is a solution, and we can implement it without changing a line of our existing code, entirely by configuring the view hierarchy in Interface Builder. The idea is to put the search bar inside a plain old UIView. That way, the table view will make sure the UIView fills up the space, but the UIView's contents will still be presented just the way we set them up.

Select BIDViewController.xib to edit the file in Xcode's Interface Builder, and select the table view in the editing area. Use the object library to find a View, and drag it to the top of the table view.

You're trying to drop the view into the table view's header section, a special part of the table view that lies before the first section. The way to do this is to place the view at the top of the table view. Before you release the mouse button, you should see a rounded, blue rectangle at the top of the table view (see Figure 8–27). That's your indication that if you drop the view now, it will go into the table header. Release the mouse button to drop the view once you see that blue rectangle.

images

Figure 8–27. Dropping a view onto the table view. Notice the rounded rectangle that appears at the top of the table view, indicating that the view will be added to the table's header.

Now, grab a Search Bar from the library, and drop it straight onto the view you just added. You'll see the familiar blue rectangle, and find that the search bar fits perfectly within the view (see Figure 8–28).

images

Figure 8–28. Dropping a search bar onto the view we previously placed in the table view header

Next, let's resize the search bar to make room for the index. First, select the search bar, and then grab the resizing handle on the right edge of the search bar and drag it to the left by 25 pixels. That should give us enough clearance for the table view's index column. You'll see a little, floating size panel while you're dragging. The search bar starts out at 320 pixels wide, so resize it down to 295 pixels wide (see Figure 8–29).

images

Figure 8–29. We grabbed the right edge of the search bar and dragged it to the left 25 pixels. Notice the tool tip on the left that helps you tell when you've resized the search bar to 295 pixels wide.

So far so good. Our next step is to do something about that unsightly white gap to the right of the search bar. Fortunately, there's another class whose background looks just like UISearchBar that we can use to fill in that space.

The navigation bar (UINavigationBar) is normally used to contain navigation elements (and you'll learn more about that in Chapter 9), but at its heart it is, after all, a subclass of UIView. This means it can be placed on screen and resized just like anything else.

Locate a Navigation Bar in the library, and drag it into the view at the top of the table view. You'll see that it also fills the entire UIView, obscuring the search bar. Double-click the Title text and delete it, leaving just the gradient background. Now go back to the dock and select the navigation bar (deleting the text may have left the Navigation Item selected, which is not what you want). With the navigation bar selected, grab the resize handle from its left edge and drag it to the right until the navigation bar is just 25 pixels wide (the same size we carved out of the search bar). You'll see that the gap is now covered, and the same smooth gradient appears all the way across the screen (see Figure 8–30). The illusion is complete!

images

Figure 8–30. We inserted a navigation bar in the table view header's view. We deleted its title and resized it to be 25 pixels wide. This looks good, and our search bar will not collide with our index.

Next, control-drag from the File's Owner icon to the table view, and select the table outlet. Repeat this procedure with the search bar, and select the search outlet. Single-click the search bar, and go to the attributes inspector by pressing ImagesImages4. It should look like Figure 8–31.

images

Figure 8–31. The attributes inspector for the search bar

Type search in the Placeholder field. The word search will appear, very lightly, in the search field.

A bit farther down, in the Options section, you'll find a series of checkboxes for adding a search results button or a bookmarks button at the far-right end of the search bar. These buttons don't do anything on their own (except toggle when the user taps them), but you could use them to let the delegate set up some different display content depending on the status of the toggle buttons.

Leave those first two unchecked, but do check the box that says Shows Cancel Button. A Cancel button will appear to the right of the search field. The user can tap this button to cancel the search. The final checkbox is for enabling the scope bar, which is a series of connected buttons designed to let the user pick from various categories of searchable things (as specified by the series of Scope Titles beneath it). We're not going to use the scope functionality, so just leave those parts alone.

Below the checkboxes and Scope Titles, set the Correction popup menu to No to indicate that the search bar should not try to correct the user's spelling.

Switch to the connections inspector by pressing ImagesImages6, and drag from the delegate connection to the File's Owner icon to tell this search bar that our view controller is also the search bar's delegate.

That should be everything we need here, so make sure to save your work before moving on. Now, let's dig into some code.

Modifying the Controller Implementation

The changes to accommodate the search bar are fairly drastic. Make the following modifications to BIDViewController.m:

#import "BIDViewController.h"
#import "NSDictionary+MutableDeepCopy.h"

@implementation ViewController
@synthesize names;
@synthesize keys;
@synthesize table;
@synthesize search;
@synthesize allNames;

#pragma mark -
#pragma mark Custom Methods
- (void)resetSearch {
    self.names = [self.allNames mutableDeepCopy];
    NSMutableArray *keyArray = [[NSMutableArray alloc] init];
    [keyArray addObjectsFromArray:[[self.allNames allKeys]
        sortedArrayUsingSelector:@selector(compare:)]];
    self.keys = keyArray;
}

- (void)handleSearchForTerm:(NSString *)searchTerm {
    NSMutableArray *sectionsToRemove = [[NSMutableArray alloc] init];
    [self resetSearch];

    for (NSString *key in self.keys) {
        NSMutableArray *array = [names valueForKey:key];
        NSMutableArray *toRemove = [[NSMutableArray alloc] init];

        for (NSString *name in array) {
            if ([name rangeOfString:searchTerm
                            options:NSCaseInsensitiveSearch].location == NSNotFound)
                [toRemove addObject:name];
        }
        if ([array count] == [toRemove count])
            [sectionsToRemove addObject:key];

        [array removeObjectsInArray:toRemove];
    }
    [self.keys removeObjectsInArray:sectionsToRemove];
    [table reloadData];
}

.
.
.
- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    NSString *path = [[NSBundle mainBundle] pathForResource:@"sortednames"
        ofType:@"plist"];
    NSDictionary *dict = [[NSDictionary alloc]
        initWithContentsOfFile:path];
    self.names = dict;
    self.allNames = dict;

    NSArray *array = [[names allKeys] sortedArrayUsingSelector:
         @selector(compare:)];
    self.keys = array;

    [self resetSearch];
    [table reloadData];
    [table setContentOffset:CGPointMake(0.0, 44.0) animated:NO];

}

- (void)viewDidUnload {
    [super viewDidUnload];
    // Release any retained subviews of the main view.
    // e.g. self.myOutlet = nil;
    self.table = nil;
    self.search = nil;
    self.allNames = nil;

    self.names = nil;
    self.keys = nil;
}
.
.
.
#pragma mark -
#pragma mark Table View Data Source Methods
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return [keys count];
    return ([keys count] > 0) ? [keys count] : 1;
}

- (NSInteger)tableView:(UITableView *)aTableView
       numberOfRowsInSection:(NSInteger)section {
    if ([keys count] == 0)
        return 0;

    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];
    return [nameSection count];
}

- (UITableViewCell *)tableView:(UITableView *)aTableView
       cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSUInteger section = [indexPath section];
    NSUInteger row = [indexPath row];

    NSString *key = [keys objectAtIndex:section];
    NSArray *nameSection = [names objectForKey:key];

    static NSString *sectionsTableIdentifier = @"SectionsTableIdentifier";

    UITableViewCell *cell = [aTableView dequeueReusableCellWithIdentifier:
        sectionsTableIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero
            reuseIdentifier: sectionsTableIdentifier] autorelease];
    }

    cell.textLabel.text = [nameSection objectAtIndex:row];
    return cell;
}

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
    if ([keys count] == 0)
        return nil;


    NSString *key = [keys objectAtIndex:section];
    return key;
}

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    return keys;
}

#pragma mark -
#pragma mark Table View Delegate Methods
- (NSIndexPath *)tableView:(UITableView *)tableView
    willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [search resignFirstResponder];
    return indexPath;
}

#pragma mark -
#pragma mark Search Bar Delegate Methods
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    NSString *searchTerm = [searchBar text];
    [self handleSearchForTerm:searchTerm];
}

- (void)searchBar:(UISearchBar *)searchBar
    textDidChange:(NSString *)searchTerm {
    if ([searchTerm length] == 0) {
        [self resetSearch];
        [table reloadData];
        return;
    }
    [self handleSearchForTerm:searchTerm];
}

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    search.text = @"";
    [self resetSearch];
    [table reloadData];
    [searchBar resignFirstResponder];
}

@end

Wow, are you still with us after all that typing? Let's break it down and see what we just did. We'll start with the first new method we added.

Copying Data from allNames

Our new resetSearch method will be called any time the search is canceled or the search term changes.

- (void)resetSearch {
    self.names = [self.allNames mutableDeepCopy];
    NSMutableArray *keyArray = [[NSMutableArray alloc] init];
    [keyArray addObjectsFromArray:[[self.allNames allKeys]
              sortedArrayUsingSelector:@selector(compare:)]];
    self.keys = keyArray;
}

All this method does is create a mutable copy of allNames, assign it to names, and then refresh the keys array so it includes all the letters of the alphabet.

We need to refresh the keys array because if a search eliminates all values from a section, we need to get rid of that section, too. Otherwise, the screen would be filled up with headers and empty sections, and it wouldn't look good. We also don't want to provide an index to something that doesn't exist, so as we cull the names based on the search terms, we also cull the empty sections.

Implementing the Search

The other new method we added is the actual search.

- (void)handleSearchForTerm:(NSString *)searchTerm {
    NSMutableArray *sectionsToRemove = [[NSMutableArray alloc] init];
    [self resetSearch];

    for (NSString *key in self.keys) {
        NSMutableArray *array = [names valueForKey:key];
        NSMutableArray *toRemove = [[NSMutableArray alloc] init];
        for (NSString *name in array) {
            if ([name rangeOfString:searchTerm
                options:NSCaseInsensitiveSearch].location == NSNotFound)
                    [toRemove addObject:name];
        }

        if ([array count] == [toRemove count])
            [sectionsToRemove addObject:key];

        [array removeObjectsInArray:toRemove];
    }
    [self.keys removeObjectsInArray:sectionsToRemove];
    [table reloadData];
}

Although we'll kick off the search in the search bar delegate methods, we pulled handleSearchForTerm: into its own method, since we're going to need to use the exact same functionality in two different delegate methods. By embedding the search in the handleSearchForTerm: method, we consolidate the functionality into a single place so it's easier to maintain, and then we just call this new method as required. Since this is the real meat (or tofu, if you prefer) of this section, let's break this method down into smaller chunks.

First, we create an array that's going to hold the empty sections as we find them. We use this array to remove those empty sections later, because it is not safe to remove objects from a collection while iterating through that collection. Since we are using fast enumeration, attempting to do that will raise an exception. So, since we won't be able to remove keys while we're iterating through them, we store the sections to be removed in an array, and after we're finished enumerating, we remove all the objects at once. After allocating the array, we reset the search.

   NSMutableArray *sectionsToRemove = [[NSMutableArray alloc] init];
   [self resetSearch];

Next, we enumerate through all the keys in the newly restored keys array.

   for (NSString *key in self.keys) {

Each time through the loop, we grab the array of names that corresponds to the current key and create another array to hold the values we need to remove from the names array. Remember that we're removing names and sections, so we must keep track of which keys are empty as well as which names don't match the search criteria.

      NSMutableArray *array = [names valueForKey:key];
      NSMutableArray *toRemove = [[NSMutableArray alloc] init];

Next, we iterate through all the names in the current array. So, if we're currently working through the key of A, this loop will enumerate through all the names that begin with A.

      for (NSString *name in array) {

Inside this loop, we use one of NSString's methods that returns the location of a substring within a string. We specify an option of NSCaseInsensitiveSearch to tell it we don't care about the search term's case—in other words, A is the same as a. The value returned by this method is an NSRange struct with two members: location and length. If the search term was not found, the location will be set to NSNotFound, so we just check for that. If the NSRange that is returned contains NSNotFound, we add the name to the array of objects to be removed later.

         if ([name rangeOfString:searchTerm
             options:NSCaseInsensitiveSearch].location == NSNotFound)
                 [toRemove addObject:name];
      }

After we've looped through all the names for a given letter, we check to see whether the array of names to be removed is the same length as the array of names. If it is, we know this section is now empty, and we add it to the array of keys to be removed later.

      if ([array count] == [toRemove count])
          [sectionsToRemove addObject:key];

Next, we actually remove the nonmatching names from this section's arrays.

       [array removeObjectsInArray:toRemove];
}

Finally, we remove the empty sections, release the array used to keep track of the empty sections, and tell the table to reload its data.

   [self.keys removeObjectsInArray:sectionsToRemove];
   [sectionsToRemove release];
   [table reloadData];
}
Changes to viewDidLoad

Down in viewDidLoad, we made a few changes. First, we now load the property list into the allNames dictionary instead of the names dictionary and delete the code that loads the keys array, because that is now done in the resetSearch method. We then call the resetSearch method, which populates the names mutable dictionary and the keys array for us. After that, we call reloadData on our tableView. In the normal flow of the program, reloadData will be called before the user ever sees the table, so most of the time it's not necessary to call it in viewDidLoad:. However, in order for the line after it, setContentOffset:animated:, to work, we need to make sure that the table is all set up, which we do by calling reloadData on the table.

   [table reloadData];
   [table setContentOffset:CGPointMake(0.0, 44.0) animated:NO];

So, what does setContentOffset:animated: do? Well, it does exactly what it sounds like. It offsets the contents of the table—in our case, by 44 pixels, the height of the search bar. This causes the search bar to be scrolled off the top when the table first comes up. In effect, we are “hiding” the search bar up there at the top, to be discovered by users the first time they scroll all the way up. This is similar to the way that Mail, Contacts, and other standard iOS applications support searching. Users don't see the search bar at first, but can bring it into view with a simple downward swipe.

Hiding the search bar bears a certain risk in that the user might not discover the search functionality at first, or perhaps not at all! However, this is a risk shared among a wide variety of iOS apps, and this usage of the search bar is now so common that there's no real reason to show anything more explicit. We'll talk more about this in the “Adding a Magnifying Glass to the Index” section, coming up soon.

Changes to Data Source Methods

If you skip down to the data source methods, you'll see we made a few minor changes there. Because the names dictionary and keys array are still being used to feed the data source, these methods are basically the same as they were before.

We did need to account for the fact that table views always have a minimum of one section, and yet the search could potentially exclude all names from all sections. So, we added a little code to check for the situation where all sections were removed. In those cases, we feed the table view a single section with no rows and a blank name. This avoids any problems and doesn't give any incorrect feedback to the user.

Adding a Table View Delegate Method

Below the data source methods, we've added a single delegate method. If the user clicks a row while using the search bar, we want the keyboard to go away. We accomplish this by implementing tableView:willSelectRowAtIndexPath: and telling the search bar to resign first responder status, which will cause the keyboard to retract. Next, we return indexPath unchanged.

- (NSIndexPath *)tableView:(UITableView *)tableView
    willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [search resignFirstResponder];
    return indexPath;
}

We could also have done this in tableView:didSelectRowAtIndexPath:, but because we're doing it here, the keyboard retracts a bit sooner.

Adding Search Bar Delegate Methods

The search bar has a number of methods that it calls on its delegate. When the user taps return or the search key on the keyboard, searchBarSearchButtonClicked: will be called. Our version of this method grabs the search term from the search bar and calls our search method, which will remove the nonmatching names from names and the empty sections from keys.

- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
    NSString *searchTerm = [searchBar text];
    [self handleSearchForTerm:searchTerm];
}

The searchBarSearchButtonClicked: method should be implemented any time you use a search bar.

We also implement another search bar delegate method, which requires a bit of caution. This next method implements a live search. Every time the search term changes, regardless of whether the user has selected the search button or tapped return, we redo the search. This behavior is very user-friendly, as the users can see the results change while typing. If users pare the list down far enough on the third character, they can stop typing and select the row they want.

You can easily hamstring the performance of your application by implementing live search, especially if you're displaying images or have a complex data model. In this case, with 2,000 strings and no images or accessory icons, things actually work pretty well, even on a first-generation iPhone or iPod touch.

CAUTION: Do not assume that snappy performance in the simulator translates to snappy performance on your device. If you're going to implement a live search like this, you need to test extensively on actual hardware to make sure your application stays responsive. When in doubt, don't use the live search feature. Your users will likely be perfectly happy tapping the search button.

To handle a live search, implement the search bar delegate method searchBar:textDidChange: like so:

- (void)searchBar:(UISearchBar *)searchBar
    textDidChange:(NSString *)searchTerm {
    if ([searchTerm length] == 0) {
        [self resetSearch];
        [table reloadData];
        return;
    }
    [self handleSearchForTerm:searchTerm];
}

Notice that we check for an empty string. If the string is empty, we know all names are going to match it, so we simply reset the search and reload the data, without bothering to enumerate over all the names.

Last, we implement a method that allows us to be notified when the user clicks the Cancel button on the search bar.

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    search.text = @"";
    [self resetSearch];
    [table reloadData];
    [searchBar resignFirstResponder];
}

When the user clicks Cancel, we set the search term to an empty string, reset the search, and reload the data so that all names are showing. We also tell the search bar to yield first responder status, so that the keyboard drops away and the user can resume working with the table view.

If you haven't done so already, fire up our app and try out the search functionality. Remember that the search bar is scrolled just off the top of the screen, so drag down to bring it into view. Click in the search field and start typing. The name list should trim to match the text you type (Figure 8–32). It works, right?

images

Figure 8–32. Our Sections app in all its glory. As promised, the index no longer steps on the Cancel button. Nice!

For our next bit of tinkering, how about making that index disappear when you tap on the search field? This is not mandatory—it's strictly a design decision—but worth knowing how to do.

First, let's add aproperty variable to keep track of whether the user is currently using the search bar. Add the following to BIDViewController.h:

@interface ViewController : UIViewController
<UITableViewDataSource, UITableViewDelegate, UISearchBarDelegate>

@property (strong, nonatomic) IBOutlet UITableView *table;
@property (strong, nonatomic) IBOutlet UISearchBar *search;
@property (strong, nonatomic) NSDictionary *allNames;
@property (strong, nonatomic) NSMutableDictionary *names;
@property (strong, nonatomic) NSMutableArray *keys;
@property (assign, nonatomic) BOOL isSearching;
- (void)resetSearch;
- (void)handleSearchForTerm:(NSString *)searchTerm;
@end

Save the file, and let's shift our attention to BIDViewController.m. First, add a method synthesizer for the new property:

@implementation ViewController
@synthesize names;
@synthesize keys;
@synthesize table;
@synthesize search;
@synthesize allNames;
@synthesize isSearching;

Next, we need to modify the sectionIndexTitlesForTableView: method to return nil if the user is searching:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
    if (isSearching)
        return nil;
    return keys;
}

We need to implement a new delegate method to set isSearching to YES when searching begins. Add the following method to the search bar delegate methods section of BIDViewController.m:

- (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
    isSearching = YES;
    [table reloadData];
}

This method is called when the search bar is tapped. In it, we set isSearching to YES, and then tell the table to reload itself, which causes the index to disappear. We also need to remember to set isSearching to NO when the user is finished searching. There are two ways a user can finish searching: by pressing the Cancel button or by tapping a row in the table. Therefore, we need to add code to the searchBarCancelButtonClicked: method:

- (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
    isSearching = NO;
    search.text = @"";
    [self resetSearch];
    [table reloadData];
    [searchBar resignFirstResponder];
}

We also need to make that change to the tableView:willSelectRowAtIndexPath: method:

- (NSIndexPath *)tableView:(UITableView *)tableView
  willSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    [search resignFirstResponder];
    isSearching = NO;
    search.text = @"";
    [tableView reloadData];

    return indexPath;
}

Now, try it again. You'll see that when you tap the search bar, the index disappears until you're finished searching.

Adding a Magnifying Glass to the Index

Because we offset the table view's content, the search bar is not visible when the application first launches, but a quick flick down brings the search bar into view so it can be used. It is also acceptable to put a search bar above the table view, rather than in it, so that the bar is always visible, but this eats up valuable screen real estate. Having the search bar scroll with the table uses the iPhone's small screen more efficiently, and the user can always get to the search bar quickly by tapping in the status bar at the top of the screen.

The problem is that not everyone knows that tapping in the status bar takes you to the top of the current table. The ideal solution would be to put a magnifying glass at the top of the index the way that the Contacts application does (see Figure 8–33). And guess what? We can actually do just that. iOS includes the ability to place a magnifying glass in a table index. Let's do that now for our application.

images

Figure 8–33. The Contacts application has a magnifying glass icon in the index that takes you to the search bar. Prior to iOS 3, this was not available to other applications, but now it is.

Only three steps are involved in adding the magnifying glass:

  • Add a special value to our keys array to indicate that we want the magnifying glass.
  • Prevent iOS from printing a section header in the table for that special value.
  • Tell the table to scroll to the top when that item is selected.

Let's tackle these tasks in order.

Adding the Special Value to the Keys Array

To add the special value to our keys array, all we need to do is add one line of code to the resetSearch method:

- (void)resetSearch {
    self.names = [self.allNames mutableDeepCopy];
    NSMutableArray *keyArray = [[NSMutableArray alloc] init];
    [keyArray addObject:UITableViewIndexSearch];
    [keyArray addObjectsFromArray:[[self.allNames allKeys]
            sortedArrayUsingSelector:@selector(compare:)]];
    self.keys = keyArray;
}
Suppressing the Section Header

Now, we need to suppress that value from coming up as a section title. We do that by adding a check in the existing tableView:titleForHeaderInSection: method, and return nil when it asks for the title for the special search section:

- (NSString *)tableView:(UITableView *)tableView
    titleForHeaderInSection:(NSInteger)section {
    if ([keys count] == 0)
        return nil;

    NSString *key = [keys objectAtIndex:section];
    if (key == UITableViewIndexSearch)
        return nil;

    return key;
}
Telling the Table View What to Do

Finally, we need to tell the table view what to do when the user taps the magnifying glass in the index. When the user taps the magnifying glass, the delegate method tableView:sectionForSectionIndexTitle:atIndex: is called, if it is implemented.

Add this method to the bottom of BIDViewController.m, just above the @end:

- (NSInteger)tableView:(UITableView *)tableView
        sectionForSectionIndexTitle:(NSString *)title
        atIndex:(NSInteger)index {

    NSString *key = [keys objectAtIndex:index];
    if (key == UITableViewIndexSearch) {
        [tableView setContentOffset:CGPointZero animated:NO];
        return NSNotFound;
    } else return index;
}

To tell it to go to the search box, we must do two things. First, we need to get rid of the content offset we added earlier, and then we must return NSNotFound. When the table view gets this response, it knows to scroll up to the top. So, now that we've removed the offset, it will scroll to the search bar rather than to the top section.

NOTE: In the tableView:sectionForSectionIndexTitle:atIndex: method, we used a special constant called CGPointZero, which represents the point (0, 0) in the coordinate system. It's a handy, readable thing, but that constant requires the use of the Core Graphics framework. When you build your project, if you get a link error complaining about a reference to _CGPointZero, you'll know that Xcode did not include that framework by default, and you'll need to add it yourself. To add this framework, go to the project navigator and select the top-level Sections item. Next, click the Build Phases tab at the top of the main pane. Then expand the Link Binary With Libraries section, click the plus button, select CoreGraphics.framework from the list that appears, and click the Add button.

Now you can build and run your app. And there you have it—live searching in an iPhone table, with a magnifying glass in the index!

TIP: iOS includes even more cool search stuff. Interested? Go to the documentation browser and do a search for UISearchDisplay to read up on UISearchDisplayController and UISearchDisplayDelegate. You'll likely find this material much easier to understand once you've made your way through Chapter 9.

Putting It All on the Table

Well, how are you doing? This was a pretty hefty chapter, and you've learned a ton! You should have a very solid understanding of the way that flat tables work. You should know how to customize tables and table view cells, as well as how to configure table views. You also saw how to implement a search bar, which is a vital tool in any iOS application that presents large volumes of data. Make sure you understand everything we did in this chapter, because we're going to build on it.

We're going to continue working with table views in the next chapter. You'll learn how to use them to present hierarchical data. You'll see how to create content views that allow the user to edit data selected in a table view, as well as how to present checklists in tables, embed controls in table rows, and delete rows.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset