Table Views – the missing guide

TableViews in iOS is like coding 101 – it should be one of the first things that you learn. Lots of apps use table views, including many of the built in apps in the iPhone. Nonetheless I’ve been struggling with them for a week and spent a good few weeks before that avoiding this task all together.  And in my debugging and trying to figure out how to do the exact thing I want to do with a table view, I’ve found that the necessary information is spread out all over the docs and some of it is just not there.

I think one reason for the lack of detailed documentation is that tableviews are one of the basic views of iOS and as such there’s almost an endless list of things that can be done with them.  But I don’t want to do any of those things.  I want to make a basic table that pops up in my app as an options or settings view.  It needs to have a DONE button and it needs to drill down to allow option selection in the same way that the Settings app does in the iPhone.

So I might as well provide some of the answers here, if for no other reason, so I can reference back to it later the next time I want to do this.  This is another long post so buckle up…

Sample Code and Starting Point

As normal I’m not going to list full sample code on here, only code snippets.  That’s not what I’m about.  But I’ll tell you which code samples to download from Apple. You’ll want to look at: SimpleDrillDown and the UICatalog.

This is ‘the missing guide’ which covers all the things I couldn’t easily find in the docs and had to learn the hard way.  So I’m also not going to detail the basics of how the TableView works cause there are lots of code examples on that and other sites that talk about it. However, I’ll brush on it. The general idea is that you override:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
     //create a cell
     return cell;
}

The method is passed indexPath which contains the .section and .row.  You create the cell that you want placed in that location in the table and return it.  Other methods are overridden to determine how many sections and how many rows you want in each section. Experiment with the simple drill down to figure out how the root view controller provides info about the cells in the root level table when asked.

The Modal TableView

One of the first problems I had was in trying to figure out how to use the table view as a options view that appears above my app.  Most of the code samples I had found were for apps where the only view is the table view.  Also many code samples use Interface Builder for the tables, and I prefer to create everything programatically.  So here’s the answer: use the TableView modally.  That will cause it to animate in to place over whatever your normal view is.  It took a while to find it, but it is in one of the docs.

In my app I have an iPhone view controller which opens an openGL view (glView).  I’m lazy so most all my app code is in the glView.  Any view that comes up completely over my glView needs to do so from within the controller that is controlling the glView.  So in my iPhoneViewController I have a notification set up so I can send a message that will call openTableView:

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(openTableView)
name:@"optionsTableView" object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(closingTableView)
name:@"closeTableView" object:nil];

When the user touches the options button in my normal app UI the message is sent, it triggers the notification and calls openTableView.   Here’s the code for that:

in .h:
   @class RootViewController;
and:
   RootViewController *rootViewController;
-(void)openTableView {
     NSLog(@"opentableview");
     rootViewController = [[RootViewController alloc]
         initWithStyle: UITableViewStyleGrouped];
     // transfer properties to the table view controller
     rootViewController.option1 = glView.option1;
     rootViewController.option2 = glView.option2;
     UINavigationController *aNavigationController =
         [[UINavigationController alloc] initWithRootViewController:
         rootViewController];
     [self presentModalViewController:aNavigationController
         animated:YES];
     [aNavigationController release];
}

Again, I’m not using a NIB to do this but doing it programatically.  Which is the way that the View Controller Programming Guide for iOS suggests to do it in the section on Combined View Controller Interfaces.  (took forever to find that reference)

I need the table view to “know” what the options are currently set to, so I have a copy of these properties set up inside the tableview and after creating the instance of the rootViewController (the table view) I set these parameters, then create a navigation controller using the rootViewController and then present it modally.

The navigation controller does all the work of handling the different views that will be included in the table view, starting with the root view.  For example, it will keep track of the stack for drill down views that get pushed on.  It also handles the animation for us.

Adding The DONE Button

Another thing that took forever to figure out was how to have a done button so that I can exit the settings view and get back to my app.  This is actually simple and is done inside the RootViewController.m’s viewDidLoad.

- (void)viewDidLoad {
     [super viewDidLoad];
     self.title = @"Settings";
     UIBarButtonItem *doneButton = [[UIBarButtonItem alloc]
          initWithTitle:@"Done"
          style:UIBarButtonItemStyleBordered
          target:self
          action:@selector(didPressDone)];
     self.navigationItem.rightBarButtonItem = doneButton;
     [self initializeTableArrays];
}

And I say that I don’t post code.. ha!  The self.title= is sometimes followed by a NSLocalizedString.  I’ll eventually do that to introduce multi-language support but not in this first crack at this.  For now I just hard code the title string.

The UIBarBurronItem is the key.  The target and action are set to a method in the RootViewController.m called didPressDone.  In there I send the ‘closeTableView’ message which is picked up by a notifier in the iPhoneViewController. One last step, the way I coded it, in order to get the option properties back they have to be moved from my options array to the individual option properties.  So I do that in the didPressDone before sending the notification.

-(void) didPressDone {
     // this is just an example - not exact code...
    option1 = [[allTableOptions objectAtIndex:1] intValue];

    NSNotification* notification =
        [NSNotification notificationWithName:@"closeTableView"
        object:self];
    [[NSNotificationCenter defaultCenter]
        postNotification:notification];
}

When the iPhoneViewController gets the message it calls closingTableView which retrieves all the new settings values that were changed and dismisses the modal:

-(void)closingTableView {
     [self dismissModalViewControllerAnimated: YES];
     glView.option1 = rootViewController.option1;
     glView.option2 = rootViewController.option2;
     [rootViewController release];
}

Debugging the Sequence of Your Code

Ever have a coding problem that is taking hours to solve only to find out that the problem goes away when you change the order of the code?  It’s happened a couple of times to me this week and since it can be an illusive bug here are things to look out for.

Invisible Switches in the TableView

The first example is when I added switches to a cell so the user can turn a feature on or off.  The UICatalog code sample shows basically how to do it.  But with my code the switch wasn’t fully showing or was mostly invisible.  If you dragged the table so the switch went off the screen and back on it would recycle the cell and then be there.  But initially it looked like this, with only a sliver of the table showing (on the right):

This took several hours to debug. Then I finally realized that something was drawing over top of the switch and erasing it.  It was the text of the label.  The solution turned out to be simple.  Place:

cell.textLabel.text = cellText;

before:

UIControl *control = [[self.sectionOneControls
     objectAtIndex: indexPath.row]
     valueForKey:kViewKey];
control.tag = tagToUse;
[cell.contentView addSubview:control];

…and not after.  When you set the textLabel.text is apparently when it draws it.  So draw it before you add the control or any other subview.

TableView Switches initially not Set Correctly

This one is fairly specific to my code and therefore is more antidotal, but it’s another example of it taking several hours to solve a problem when the solution was simply the order of the code.  The problem was that when the table appeared the switches were not set to the correct on/off value.  I verified that the property was properly moved to the RootViewController.  The correct value made it there.  The key was that I stored this value off in an options array so that it could be accessed via the cell index.  But the array’d value wasn’t making it to the method building the UISwitch in time.  I used NSLog’s to figure out what was happening in what order and discovered that when I switch the order of assignments in my initializeTableArray’s method suddenly the switches were showing up correct.

One option would be to leave it and move on.  But I feared that without knowing exactly why it was happening it might be intermittent based on how fast the UI built the table and requested the switches UIControls. So I continued to research, adding lots of NSLogs and reordering the code back so that it would fail again.

Also important is that I had messed around with different ways to hold the data for what goes in each cell of the table, which cells have switches, and what options are available for drill down subview tables. I finally decided to mix what was done in the two code examples I mentioned above.  For switches and other controls I borrowed from the UICatalog and created an NSArray filled with dictionary objects so that I can use a key to reference a control for the switch:

// Display Timer Clock
[NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:kDISPLAYTIMER], kTagKey,
[NSNumber numberWithInt:CELL_STYLE_CONTROL], kStyleKey,
self.timerSwitchCtl, kViewKey,
nil],

Inside the method timerSwitchCtl I was accessing the property of whether the switch should start on or off via that other options array.  And the placement of the assignment of that options array was important. If it was above the assignment of the NSArray the switch would be set correctly, if it was after it would be set incorrectly.

What I had assumed was that the switch didn’t get built until the cell for that table was created and the

[cell.contentView addSubview:control];

was called. But that’s where I was wrong. The timerSwitchCtl method is called the moment it is referenced in the NSArray assignment.  If the NSArray was assigned before the options array the switch would be at it’s default because the options array hadn’t been built yet.  Duh!  Wow.. took almost longer to explain than it did to solve it.

viewWillDisappear

I ended up reworking my code so that I’m no longer using viewWillDisappear, but when I was using it I again found a sequence problem. So it might be helpful to note the order that things execute in.  You can trace the following in the code snippets I included above, or in your own code you can use a ton of NSLogs in each method to see what order they execute in.  Let’s say that in my RootViewController I added a viewWillDisappear method.  When the user taps the Done button on the TableView whatever action I had set up on the UIBarButton will go first.  For me this is didPressDone. Inside there I’m sending a message which is picked up by a notification in the iPhoneViewController, which is calling closingTableView.  Inside of that I’m dismissing the modal which causes the root view of the table to close, and right before it does it executes viewWillDisappear. Get all that?

If you have any code in viewWillDisappear it’s important to know when in the sequence of events it will execute.  At one point in there I had my options values transfer from my options array back to the individual properties.  But it was happening after my attempt to move those values back to glView.option1, etc.  So I moved this code to the didPressDone method, and now it happens immediately after the user taps Done.

Displaying an option that takes you to a
list of choices like they have in the Settings app…

The whole point to my tableView is to be like the Settings app.  Here’s a picture of what I was trying to achieve:

Then when you tap that you get a list of choices to pick from:

Then when you pick one and return to the root view the new selection is there.  While this looks like it should be easy it is a fairly complex task. Because I don’t want to write a book here I’m going to breeze through it as briefly as I can. Again this isn’t a complete guide, it’s only a collection of what was missing in the docs.

Table View Cell Styles

First, how to do the cell that has the black label on the left and the currently selected option in blue on the right.  Thankfully Apple has provided some default styles to help with this. This is a default style called: UITableViewCellStyleValue1.  Without getting into the complexities of my custom data structures, here’s how you create a cell with that style and how you set those two labels:

static NSString *kOptionCell_ID = @"OptionCellID";
cell = [self.tableView
  dequeueReusableCellWithIdentifier:kOptionCell_ID];
if (cell == nil)
{
  cell = [[[UITableViewCell alloc]
    initWithStyle:UITableViewCellStyleValue1
    reuseIdentifier:kOptionCell_ID] autorelease];
  cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  cell.selectionStyle = UITableViewCellSelectionStyleGray;
}
cell.textLabel.text = @"Language";
cell.detailTextLabel.text = @"English";
return cell;

The accessoryType listed above gives you that “>” chevron, and I’ve chosen a selection style of the more subtile gray instead of the standard blue.

Data structures for option details

Of course in real code you’d store all the details for each cell in some sort of array or dictionary.  This way of storing code is fairly critical to minimizing code. Like I said before I used a combination of the ways done in the two Apple code samples I referenced above.  I also have a custom data class based on NSObeject that holds all the data for an individual option (like Languages). It has properties for the option title (Languages), the list of available options which I called labels (English, French, Spanish), and one to tell what the currently selected option is (0,1,2,3,etc).  Look in the SimpleDrillDown example to see how they did it.  They used a DataController, I did not.  But I did create an NSMutable array to hold several “option” instances.  The Nth item in the array is the Nth item in my table.

Also much like the SimpleDrillDown example, when the next table is being created you can pass the options object down to it, which it can use as it’s data source. When the user selects an option you set the selected property so that when they go back up to the root view you’ll know which item to stick in detailTextLabel.text.

Here you go.. the RootViewController.m’s drill down code:

- (void)tableView:(UITableView *)tableView
      didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  // When a row is selected, create the detail view controller
  // and set its detail item to the item associated
  // with the selected row.
  OptionsViewController *optionsViewController =
       [[OptionsViewController alloc] initWithStyle:
       UITableViewStyleGrouped];
  optionsViewController.options =
       [allTableOptions objectAtIndex:indexPath.row];
  indexRowBeforePush = indexPath.row;
  // Push the detail view controller.
  [[self navigationController]
       pushViewController:optionsViewController animated:YES];
  [optionsViewController release];
}

OptionsViewController is my subview table view to handle the options for a setting. It has an “options” property as well, so we set it to the one in our options array for the selected row.  We also store which row was selected when we moved on to the next table view.  This will come in handy in a minute. Then we push the next view controller (the options view controller).

The checkmark

Once you get into the OptionsViewController’s code it’s actually fairly simple. Create cells for each of the available options with the currently selected one checkmarked. The checkmark is also a convenience property of the table view cell.  Because I’m getting tired and because I haven’t found anyone else nice enough to post this code, I’m just going to post the code:

- (UITableViewCell *)tableView:(UITableView *)tableView
      cellForRowAtIndexPath:(NSIndexPath *)indexPath {
   static NSString *CellIdentifier = @"CellIdentifier";
   UITableViewCell *cell = [tableView
      dequeueReusableCellWithIdentifier:CellIdentifier];
   if (cell == nil) {
      cell = [[[UITableViewCell alloc]
         initWithStyle:UITableViewCellStyleDefault
         reuseIdentifier:CellIdentifier] autorelease];
      cell.selectionStyle = UITableViewCellSelectionStyleGray;
      if (indexPath.row == [options.selected intValue]) {
         cell.accessoryType = UITableViewCellAccessoryCheckmark;
      }
   else {
      cell.accessoryType = UITableViewCellAccessoryNone;
   }
}
cell.textLabel.text = [options.longLabels
   objectAtIndex:indexPath.row];
return cell;
}

The accessory type UITableViewCellAccessoryCheckmark places a checkmark to the right of the cell and we only set it for the row that is selected.  When the user taps a selection we swap the selection:

- (void)tableView:(UITableView *)tableView
     didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  // Unset the accessory view for the previous selection.
  NSIndexPath *selectionIndexPath = [NSIndexPath 
     indexPathForRow:[options.selected intValue] inSection:0];
  UITableViewCell *checkedCell = [tableView 
     cellForRowAtIndexPath:selectionIndexPath];
  checkedCell.accessoryType = UITableViewCellAccessoryNone;
  options.selected = [NSNumber numberWithInt:indexPath.row];
  // Set the checkmark accessory for the selected row.
  [[tableView cellForRowAtIndexPath:indexPath]
      setAccessoryType:UITableViewCellAccessoryCheckmark];    
  // Deselect the row.
  [tableView deselectRowAtIndexPath:indexPath animated:YES];
}

I found the way to do this buried in some other sample code somewhere. And now you have almost all of my OptionViewController code.  All the work is done in there. It unselects the row they tapped, turns off the checkmark for whatever row had it before, and turns it on for this row… with animations. The option.selected = ….row sets which option was chosen, which is really setting it in the RootViewController’s data structure That’s why the data structure is important to figure out, and why it’s a mutable array. There isn’t much more to it. The back to settings button is handled by the navigation controller that all this is wrapped in.

Refreshing the root table view’s view

When pop’d back up to the root view, you want the cell they tapped on to now have the new options they selected in the optionsViewController. Because of the data structure, if the cell were to refresh it would display the correct thing, but unless there was a memory warning the root view was cached and will still have the old value.  Problem solved… in the RootViewController.m:

- (void)viewWillAppear:(BOOL)animated
{
    //NSLog(@"view will appear");
    NSIndexPath *tableSelection =
        [self.tableView indexPathForSelectedRow];
    [self.tableView deselectRowAtIndexPath:tableSelection
        animated:NO];
    [self.tableView reloadData];
}

The tableView reloadData will force all the cells to refresh which will make the newly selection option to appear in the detailTextLabel.text.

Conclusion…

This is an over 3000 word post on the stuff I discovered that wasn’t easy to find in the docs.  What else is there to say.  There’s probably 30,000 more words that could explain all the other things to do with table views.  Enjoy!

Leave a Reply

Your email address will not be published. Required fields are marked *