Wednesday, 1 February 2012

UITableView How-To: Part 1 - View Controller Setup

Part 2 | Part 3 | Part 4 | Part 5

6 months ago, I was preparing for a semi-major restructuring of SlickShopper. My 1.0 had hard-coded sizes everywhere, but I wanted to support screen rotation, so something had to change. I was in the process of rebuilding things using Interface Builder when I made some key discoveries about UINavigationControllers (they have a toolbar property) and UITableViewControllers (they inherently support resizing due to rotation). I bailed on the IB stuff, and started over using table view controllers exclusively. Version 1.5 onward contains table view controllers exclusively. I patted myself on the back for so deftly having avoided IB.

Today, I'm long past my fear of IB, and I've mostly given up on pure table view controllers. I use IB as much as possible, and prefer to set everything up as a plain view controller. I enjoy the flexibility this provides. (Tip: Don't name your view controllers as SomethingTableViewController, because the 'Table' part becomes incorrect if you change your mind about implementation later) I encounter a number of people struggling with basic aspects of table views, so I'm going to pool together the techniques I've learned from books and Apple's sample programs.

This will be the first installment of several posts devoted to the creation of plain view controllers that feature a table view. Today I'm going to focus on basic setup of the view controller. Future installments will look at using IB-based table cells, how to implement a search bar, and any other useful things I think of along the way. Please feel free to post requests in the comments area. My intention is to be as step-by-step as necessary, but I will also attempt to include a sample project at the end of each post.

INITIAL SETUP



I'm not really going to go into the various places this view controller could be used. Theoretically, it should be perfectly usable in a view-based app, a navigation-based app, tab-based app, etc. So, create a new project using whatever template you like. I'm going to use the navigation-based template, as I intend to show how to pass data to a sub-controller at some point in this series. But feel free to use whatever template you want, as the choice doesn't really impact what I'm going to do here.

After the project has been set up, create a new file.



Choose a UIViewController subclass, and hit the toggle to indicate that you want to use a XIB file.



I'm calling mine SampleViewController, you can call it whatever you want. The XIB checkbox is relatively new, so if you don't have it, simply create your own XIB file, and give it the same name. And then start downloading the newest version of Xcode.

We need to do work in all 3 files that were just created, but in order to avoid bouncing around I'm going to work in this order: .h -> .xib -> .m.

PREPARE THE HEADER



Your .h file should look like this:

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController {

}

@end


Since this is a plain view controller, we need to add some information to it in order to work with a table view. The parts that we are about to add are included for free with table view controllers, hence the appeal. But it is easy enough to add manually, so here we go.

First, we need to adopt a couple of protocols. Table views are designed to be generic, and rely on other objects to provide customization. For our immediate use, customization mostly refers to providing content, but it can also apply to appearance. The necessary protocols are UITableViewDataSource, and UITableViewDelegate. Data source, as the name implies, provides the content. The delegate pattern is used throughout Cocoa, and indicates that one object will be doing work on behalf of another object. There are two protocols because they serve different needs, and if desired they could be distinct objects. We are already in a class that is perfectly capable of doing the work, but if you had a reason to do so, you could certainly make two more external classes to accomplish the same thing. For simplicity, generally speaking you'll just use your view controller. To indicate that your view controller can serve in these roles, add this to the .h file:

#import < UIKit/UIKit.h >

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{

}

@end


There are no required methods in UITableViewDelegate, but there are in UITableViewDataSource. So, if you build your project before we finish up, you will get some warning messages related to the absence of those required methods. You can ignore the warnings for now, but by the time we finish up, make sure the warnings are gone.

We are going to graphically place a table view into our main view in Interface Builder, just as if we were placing a button or a label. We will have a reason to talk to that table view object. The way to establish that line of communication is to declare IBOutlets here in the .h file. We need to declare a table view instance variable, and we'll communicate with it using a property, like this:

#import < UIKit/UIKit.h >

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
   UITableView *mainTableView;
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;

@end


You may have seen the "IBOutlet" tag on the first line, and you may have seen it on both lines. Apple's examples are shifting towards showing it only on the second line, so that's what I will do. Showing it in both places is not necessary.

I recommend that you not call it "tableView". It's probably not a huge deal necessarily, but the delegate methods will use "tableView" as a local variable name, so best to avoid the clash. Also, table view controllers automatically have a tableView property, so using a different name helps to reinforce that we aren't using a table view controller.

We're basically done now, but before we move on, let's take care of the structure that will hold our contents. I'm just going to use an array for now, as arrays work quite well with the way table views expect to receive information.

#import 

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
   UITableView *mainTableView;
 
   NSMutableArray *contentsList;
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *contentsList;

@end


LAY OUT THE INTERFACE



Open up the XIB file. We want the view to be capable of supporting landscape, so we need to change the resizing masks. Select the view, then hit Cmd-3 to bring up the size inspector. Toggle the masks as shown here so that the view is fully flexible, and go ahead and make the height 480 for good measure.



If you are unable to make these changes, hit Cmd-1, turn off any simulated UI elements like the status bar, then come back and try again. (Disclosure: I only figured that out just now while typing this up... I thought it was a bug. I've been deleting and re-creating the view for quite some time)

Drag a table view from the palette onto the view. It should automatically expand to fill the entire view.



The resizing masks should already be set, but go ahead and verify them for the table view just in case.



In order to talk to the table view, we need to use the IBOutlet that we declared in the .h. To do that, Right-click (Ctrl-click) on File's Owner, and drag to the table view. (I've resized the view for sake of screen capture here)



You should see the table view highlight, and you should see "Table View" appear in a little box at the lower right, thus confirming your selection. When you let go, a window will appear:



Select the name of the IBOutlet that was created in the .h file.

We're not quite done yet. Remember the data source and delegate from the .h file? That declaration simply published the fact that our view controller is willing to serve that role. But that alone does not mean that the table view knows who to talk to. There could be any number of conforming classes eligible, so we need to identify specifically which class(es) this table view will use.

Select the table view, and hit Cmd-2. At the top of the inspector are outlets for the delegate and dataSource. Select the circle next to each one, and drag to File's Owner (regular left-click drag).





So, the view controller is able to talk to the table view, the table view will request information from the view controller, and the whole thing is resizable. Thus concludes our trip to IB.

IMPLEMENT



Now for the hard part. Most of what we will be doing in the .m file would be exactly the same or very similar if we were using a table view controller instead. Let's start at the top and work our way down.

First, synthesize the properties:

#import "SampleViewController.h"

@implementation SampleViewController

@synthesize mainTableView;
@synthesize contentsList;


Most examples show dealloc at the bottom, but I prefer to have it up top so I can quickly glance at the properties. Follow memory management rules and release the properties.

- (void)dealloc
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   [mainTableView release], mainTableView = nil;
   [contentsList release], contentsList = nil;
 
   [super dealloc];
 
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}


Somewhere we need to build the data that will appear in the table view. For a simple case like this, viewDidLoad will work just fine.

- (void)viewDidLoad
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   [super viewDidLoad];
 
   NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:@"Red", @"Blue", @"Green", @"Black", @"Purple", nil];
   [self setContentsList:array];
   [array release], array = nil;
 
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}


Build an array, stick it into the property, then release it.

For this example, we aren't too worried about the displayed contents being incorrect. But in a real app, if activity in another view controller could cause the contents here to change, we want to make sure that the user sees the updated information. To make the table refresh every time the view is displayed, we'll use viewWillAppear.

- (void)viewWillAppear:(BOOL)animated
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   [super viewWillAppear:animated];
 
   [[self mainTableView] reloadData];
 
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}


I mentioned before that UITableViewDataSource has some required methods, so let's get those out of the way. First, the table is going to ask the data source how many rows are involved. The answer should be based on our array, and we use this method to respond to the table's question:

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   NSInteger rows = [[self contentsList] count];
 
   NSLog(@"rows is: %d", rows);
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
   return rows;
}


Note the section variable that we aren't using here. So far, we only have one section, so there isn't a need to worry about it. In a future post, I'll show how to do a multi-section table, at which point this method gets a bit more involved.

Next, the table is going to ask for a view to display in each row. Apple has provided a UIView subclass called UITableViewCell that is pre-configured for many common table needs. You create a cell, give it some content, and then give that cell to the table. Repeat as needed for each row. A lot of explanation is going to be needed here, so let's jump to the end result, and I'll discuss afterwards...

- (UITableViewCell *)tableView:(UITableView *)tableView
   cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   NSString *contentForThisRow = [[self contentsList] objectAtIndex:[indexPath row]];
 
   static NSString *CellIdentifier = @"CellIdentifier";
 
   UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
   if (cell == nil)
   {
      cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
      // Do anything that should be the same on EACH cell here.  Fonts, colors, etc.
   }
 
   // Do anything that COULD be different on each cell here.  Text, images, etc.
   [[cell textLabel] setText:contentForThisRow];
 
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
   return cell;
}


In summary:
  • Grab the object that has (or in this case, is) the information we want to display for this row
  • Ask the table view if any cells are available for recycling.
  • If not, create a new cell
  • Specify the content for the cell


I'll talk more about cell customization at a later date, but for now please note the comments I added regarding where you should customize different elements of the cell.

First, a little bit about the NSIndexPath. If you were trying to describe the location of a point on a grid, you would most likely use (X,Y) coordinates. The index path provides a way of describing a location within the table, but instead of (X,Y) coordinates, it is using (section, row) coordinates. These are numbered in the exact same way an array is, so the first item is 0, the second item is 1, and so on. We only have one section, so we will only be dealing with section 0 for now. Within our only section, we are providing five pieces of information, so we'll be talking about row 0, row 1,....up to row 4.

So this method begins with the table asking the question "Hey, I'm now at the first section and the second row... what should I show here?" We need to figure out which row is being requested, and we do that by asking the indexPath for its row value. [indexPath row] (later we'll do the same thing to get a section value). Assuming we are building the table up from scratch, we should be dealing with the first row, so row 0. Now I know which piece of information I want from the array: the first item, so the item at index 0. For convenience I assign that to a local variable.

Next I declare a string variable. I don't actually know what "static" technically means, other than the obviously implied "this does not change". I suppose you could #define a constant instead. The purpose of this string will be to allow the table view to identify cells in a queue that it will create.

The table's cell queue exists because flinging your way through a list of data needs to happen as fast as possible, but building a cell from scratch can be expensive from a performance standpoint. If we were providing 1000 strings for this table, we don't want to actually create 1000 cells. We only need to create enough to cover the visible screen, plus a couple extra for buffer, and then we can reuse those cells over and over again. As a cell slides off the top of the screen, it goes into the queue, only to reappear at the bottom of the screen with new content. We don't want to build new cells if it can be avoided.

Thus, the next thing we do is ask the table if any cells are available for reuse, here:

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];


We use the string identifier, because there could be multiple kinds of cells being stored, so we want to make sure we get the right kind. If a cell is available, it will be provided to the cell variable. However, if there aren't any cells available (as would be the case when starting from scratch), the return from this call is nil. So, we find out if we actually have a cell right now:

if (cell == nil)
{
   cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
   // Do anything that should be the same on EACH cell here.  Fonts, colors, etc.
}


If there is a cell, this part doesn't happen. If there isn't a cell, this part will build a new one.

Lastly, we provide our content:

// Do anything that COULD be different on each cell here.  Text, images, etc.
[[cell textLabel] setText:contentForThisRow];


Standard cells have a label property, and I'm setting the text using our content string. I'm going to again emphasize the comments I've put in there. When you get to this point of the method, you have 2 possibilities: 1) The cell is brand new, or 2) The cell has been recycled. Recycled cells will most likely still have their old content, so you cannot make assumptions about the state of the cell you are working with. It could be pristine, it could be dirty. So this area of the method MUST make sure the end result is correct. If you alternate font colors between red and green, then you should have an if/else statement here that makes the font red OR makes the font green. You cannot assuming the incoming color is correct.

That covers the required methods for the data source. There are many, many other optional methods - from dataSource and from delegate - for performing a variety of tasks, but for now I just want to draw attention to one of them.

If you build-and-run your app, you should see the list of colors in your table. If you tap a row, it will stay highlighted. Let's turn that off. This method is how the table says "Hey, I was touched here... what do you want me to do about it?"

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSLog(@">>> Entering %s <<<", __PRETTY_FUNCTION__);
 
   [tableView deselectRowAtIndexPath:indexPath animated:YES];
 
   NSLog(@"<<< Leaving %s >>>", __PRETTY_FUNCTION__);
}


We will do a lot more with this method later, but for now we'll simply deselect the row.

And thus concludes this edition of table talk. Tune in, uh... later... for the next installment. In the meantime, here is the sample project for this stage of the exercise.

TableViewTutorial_Part1.zip

0 comments:

Post a Comment

 

Copyright @ 2013 PakTechClub.