Selecting multiple UITableView rows

Jul 25, 2011 10:01 · 920 words · 5 minute read iOS objective-C

UITableView supplies a whole variety of built-in methods to manage the table, including the selection and deselection of rows. However, there’s a significant limitation with the published API - it’ll only allow you to select one row at a time. That’s something of a problem if you want to use the table view as a list which supports selection (and the subsequent use of) multiple rows at once.

UPDATE - This has been completely superceded by iOS5, which does support multiple selection. This is not enabled by default, though - it has to be set either in Interface Builder, or in code with [tableView setAllowsMultipleSection:YES]; Thanks, Apple.

There is apparently an undocumented API call to do exactly this, but that’s no use if you’re planning to submit the app to the Store - Apple will bounce the submission. Here’s a “legal” workaround that’s a bit more complicated than a single API call, but does the trick.

# Setting up

First, you’ll need an instance variable to hold a mutable dictionary that contains references to the rows that have been selected (it needs to be mutable because you’re going to be adding and removing objects to it). This needs to be declared in your class’s header file:

NSMutableDictionary *selectedRows;

Then you’ll need to instantiate this mutable array somewhere before you can start using it - it doesn’t really matter too much where you do that, so for convenience I tend to do it in the viewDidLoad method because I can guarantee that this method always gets called:

- (void)viewDidLoad {




  [super viewDidLoad];

  /*
  Your other code goes here
  */

  // Set up the array of selected devices
  selectedRows = [[NSMutableDictionary alloc] init];

}

And then you’ll want the corresponding memory management in the viewDidUnload method:

- (void)viewDidUnload {
  /*
  Your other code goes here
  */

  // Release the selectedRows ivar
  [selectedRows release];
  selectedRows = nil;

  [super viewDidUnload];
}

# The science bit

Now let’s do the heavy lifting. Assuming that your class adopts the UITableViewDelegate protocol, whenever a table view cell is tapped it will trigger the tableView:didSelectRowAtIndexPath: method. This takes two parameters - the tableView itself, and the indexPath of the cell that’s just been tapped. We can exploit this to make the tableView handle multiple selections.

Firstly, we need to get a reference to the cell that’s been tapped:

UITableViewCell *theSelectedCell = [tableView cellForRowAtIndexPath:indexPath];

This will allow us to grab some content from the cell which can then be stuffed into the array of selected cells. In my app, I’m using cells of type UITableViewCellStyleSubtitle which have two text elements - textLabel and detailTableText. textLabel stores a device name, and detailTableText stores a unique ID for that device - as I’m going to use this unique device ID to communicate with it later, that’s the value that I’m going to store.

To create the key for the dictionary element, we’ll have to convert the indexpath row into a string:

NSString *keyString = [NSString stringWithFormat:@"%d", indexPath.row];

Before we can start grabbing any information from the cell, we need to figure out whether the cell’s already been selected. To mark the cell as selected, and at the same time display that fact to the user, we’re going to use the checkmark accessory which appears at the right hand end of the cell. This can be switched on like this:

theSelectedCell.accessoryType = UITableViewCellAccessoryCheckMark;

and switched off like

theSelectedCell.accessoryType = UITableViewCellAccessoryNone;

Now can also use the presence or absence of the accessory to determine whether the cell’s been selected or not:

if ( theSelectedCell.accessoryType == UITableViewCellAccessoryNone ) {

  // This cell hasn't already been selected - switch on the checkmark

} else {

  // This cell has already been selected - so switch OFF the checkmark

}

If the cell HASN’T been selected, we need to do two things: switch ON the accessory checkmark, and add the cell (or rather, some content from the cell) to the mutable array of selected cells that we set up earlier. If the cell HAS been selected, then we need to do the exact opposite - which looks like this:

if ( theSelectedCell.accessoryType == UITableViewCellAccessoryNone ) {

  // This cell hasn't already been selected - switch on the checkmark
  thisCell.accessoryType = UITableViewCellAccessoryCheckmark;

  // Grab the peerID from the cell's detailTextLabel
  NSString *peerID = thisCell.detailTextLabel.text;

  // Add this peerID to the array of selections at the location
  // that corresponds to the cell's position in the table
  [selectedRows setObject:peerID forKey:keyString];

} else {

  // This cell has already been selected - so switch OFF the checkmark
  thisCell.accessoryType = UITableViewCellAccessoryNone;

  // And remove the peerID from the array of selections
  [selectedRows removeObjectForKey:keyString];

}

# The end result

What we’ll end up with once the user has finished the selection is a mutable dictionary of the cells that they’ve selected - and we can use that to drive whatever action comes next. In my case, it’s sending a message to the selected devices, but it could be as simple as dumping a list of them to the console:

NSLog(@"The selected devices are:");

for (NSString *deviceID in selectedRows) {

  NSLog("%@", deviceID);

}

This process uses the accessoryType as the flag for cell selection, but there’s no reason why you can’t use any other cell property - so rather than use a checkmark, I could have changed the background colour, or formatted the text and so on. The only restriction is that it needs to be something that can be either “on” or “off” so that it can act as a flag.