Tuesday, July 10, 2012

UITableView How-To: Part 6 - Inserting, Deleting, and Reordering Cells

Part 1Part 2Part 3Part 4 | Part 5

 

It seems to be time for my annual table view tutorial post.  In this long-overdue episode, we'll take a look at what is involved with making a table view support dynamic content.  If you want to allow the user to add, remove, or rearrange cells, then these are the basics that you need to understand.

 

One of the first changes to make is to use a proper data container.  If you've been using an NSArray up to this point, then you need to switch to NSMutableArray, or and/or switch NSDictionary to NSMutableDictionary.  You can add and remove items to/from the mutable versions, but cannot from non-mutable versions.

 

Create a new project using whatever template you want.  I'm using the Single View one for simplicity sake.  Place a table view on it, and get it wired up with a property.  In addition to that, place 2 buttons on the screen, one called Edit, and the other called Insert.  It doesn't need to be fancy, just something like this:

 

xib_layout.png

 

Declare IBActions for each button, and wire them up.

 

- (IBAction)insertButtonPressed:(UIButton *)button;
- (IBAction)editButtonPressed:(UIButton *)button;

 

Our initial data is just going to be a handful of strings.  Declare a property, synthesize it, then customize a getter:

 

@property (nonatomic, retain) NSMutableArray *contents;
...
@synthesize contents = ivContents; ...
- (NSMutableArray *)contents { if (ivContents == nil) { ivContents = [[NSMutableArray alloc] initWithObjects:@"A", @"B", @"C", @"D", @"E", nil]; } return ivContents; }

 

And let's quickly set up the table view delegate and data source methods.

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
   NSInteger rows = [[self contents] count];
   return rows;
}

 

The number of rows is the count of the array.  Easy enough.

 

- (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]; }
NSString *rowContent = [[self contents] objectAtIndex:[indexPath row]];
[[cell textLabel] setText:rowContent];
return cell; }

 

And then to populate the cell, we grab the string out of the array, and give it to the cell's label.  Go ahead and run the app to make sure that the table fills in correctly.  Beware that if you press either button, it will crash right now since we haven't implemented those methods yet.

 

initial_run.png

 

Insert Row

 

Let's start with inserting rows.  The UITableView method for inserting rows is insertRowsAtIndexPaths:withRowAnimation:.  First thing to note is that this has an animation property.  You can slide rows in from above, below, either side, or simply fade in.  The other thing to note is the plural - rowS - meaning that you can insert more than 1 row with this method if you want to.

 

Let's continue by implementing our insert method.  The method above deals with index paths, so the first thing we're going to do is build one.  We're making the executive decision right now that any new rows will appear at the top of the table, and this is row 0.  A handy construction method is indexPathForRow:inSection:.  Note that it has a section parameter.  We are only using a single section at the moment, so that would be section 0.  Let's begin:

 

- (IBAction)insertButtonPressed:(UIButton *)button
{
   NSInteger section = 0;
   NSInteger row = 0;
   NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
NSArray *indexPathsToInsert = [NSArray arrayWithObject:indexPath];
[[self mainTableView] insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationRight]; }

 

We create an index path, then create an array to hold it, and finally pass that array along to the table view via the insert method.  If you run the app now, and hit the Insert button, you will again crash.  But this time there is an actual problem we need to solve.  Look in the console, and you'll find one of the most helpful error messages in all of iOS development:

 

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (5), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

 

This crash message tells you exactly what went wrong, but it does take a little bit of getting to used in order to understand what it is saying.  Basically, the table is saying "Hey! I knew from before that you had 5 rows, and then you just told me to add 1, so I did that, but after I did that, there were still only 5 rows!"  Well clearly you are stupid, Mr. Table View, because you obviously should have 6 rows now, since I told you to add one.

 

Or should it?  How does the table view know how many rows should be there?  Oh, right, from this:

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
   NSInteger rows = [[self contents] count];
   return rows;
}

 

The array still only has 5 objects.  Even though we inserted a row, the array still has just 5 objects.  The table view will ping this method again as a part of the insert process.  If 5 (old) + 1 (inserted) does not equal 6 (new total), then it will complain.  We only have a total of 5, so that's a problem.

 

This is a very important concept to understand, and it is equally important when we delete and rearrange rows in a few minutes.  Inserting a row is a view operation.  Remember the old, trusty MVC concept, Model-View-Controller?  What we did so far is make a change to V, the view.  So what still needs to happen is to make a change to M, the model.  If you insert a row in a table, you must also add something to the array.  If you remove a row from a table, you must also remove something from the array.

 

So let's revisit the insert method, and add something to the array.  Just to be quick, let's add a string that is simply a count of the number of items in the array at the time.

 

- (IBAction)insertButtonPressed:(UIButton *)button
{
   NSInteger section = 0;
   NSInteger row = 0;
   NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
NSString *extraContent = [NSString stringWithFormat:@"%d", [[self contents] count]]; [[self contents] insertObject:extraContent atIndex:row];
NSArray *indexPathsToInsert = [NSArray arrayWithObject:indexPath];
[[self mainTableView] insertRowsAtIndexPaths:indexPathsToInsert withRowAnimation:UITableViewRowAnimationRight]; }

 

Run the app again, and you should be able to hit the insert button as many times as you want.  Each time, a new row will appear at the top of the list, sliding in from the right.

 

after_insert.png

 

Some additional exercises for you to perform on your own:

  • Use different animation options to see how each one works.
  • Use a different row value to insert at different points in the table.  Beware that you should not use an index larger than the array.  You will need much smarter code than this is to protect against that.

 

Delete Row

 

There are 2 basic UI's for deleting rows.  There is swipe-to-delete, and then there is putting the table view into edit mode, where all of the red circles appear.  If you try to swipe on your rows right now, nothing will happen.  Swiping is not enabled by default, you have to implement a datasource method to get it.  That method is:

 

- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
}

 

The mere presence of this method enables the swipe-to-delete UI.  It doesn't do anything yet, but you can go ahead and run your app, swipe on a row, and you will see the delete button.  Now we need to add the guts to this method.

 

The first item to note is the editingStyle parameter.  A quick trip to the documentation reveals that the value could be none, insert, or delete.  I don't believe I have ever used this parameter for anything other than delete.  Still, better safe than sorry:

 

if (editingStyle == UITableViewCellEditingStyleDelete)

 

For inserting a row, we had to build an index path.  In this case, we are given an index path parameter already.  The table view delete row method is deleteRowsAtIndexPaths:withRowAnimation:.  Basically, the exact opposite of the insert method.  Instead of flying in from the right, the row will fly out to the right, and so on.

 

Remember from inserting a row that we must also make a change to the array.  This is no different.  We will remove a row, therefore we must remove something from the array at the same time.

 

- (void)tableView:(UITableView *)tableView
commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
forRowAtIndexPath:(NSIndexPath *)indexPath
{
   if (editingStyle == UITableViewCellEditingStyleDelete)
   {
      [[self contents] removeObjectAtIndex:[indexPath row]];
NSArray *indexPathsToRemove = [NSArray arrayWithObject:indexPath];
[tableView deleteRowsAtIndexPaths:indexPathsToRemove withRowAnimation:UITableViewRowAnimationRight];} } }

 

We remove an item from the array, then delete the corresponding row.  Run the app now, and you will be able to swipe-to-delete anything you want.  If you delete everything, just hit the insert button to put more stuff back in and then go delete it again.

 

Swipe-to-delete is nice, but sometimes it is better to show all of the delete circles to let the user see that deleting can be done.  This is done by turning on the edit mode for the table view.  By default, edit mode is off, so we will need to turn it on.  This is where our other IBAction method comes in:

 

- (IBAction)editButtonPressed:(UIButton *)button
{
   [[self mainTableView] setEditing:![[self mainTableView] isEditing] animated:YES];
}

 

We look at whether or not the table view is currently in edit mode, and then set it to the opposite.  If you run the app now, you can hit the Edit button multiple times to turn on and off edit mode.  You can also go ahead and delete rows while you're there.

 

You've probably seen apps where the button says Edit sometimes, and Done sometimes.  Let's make that happen:

 

- (IBAction)editButtonPressed:(UIButton *)button
{
   [[self mainTableView] setEditing:![[self mainTableView] isEditing] animated:YES];
NSString *buttonTitle = ([[self mainTableView] isEditing]) ? @"Done" : @"Edit";
[button setTitle:buttonTitle forState:UIControlStateNormal]; }

 

For additional research that I'm not going to go into here, if you are using a navigation bar or a toolbar, you can use UIViewController's built-in editButtonItem.  As noted in the documentation, you will need to implement setEditing:animated:, and then do basically the same thing I've done here (except for the button title part).

 

Insert And Delete Rows

 

For more complicated tables, a single insert command or a single delete command may not be sufficient.  Perhaps you will need to add AND remove some rows.  Or maybe you will add rows AND add sections (there is a different method for that, see the documentation).  If you will be performing multiple actions, then you need to group those activities inside of a table animation block.

 

Drag another button onto the view, and call it "Silly".  Then implement this method:

 

- (IBAction)sillyButtonPressed:(UIButton *)button
{
   NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:0];
   NSArray *indexPaths = [NSArray arrayWithObject:indexPath];
[[self mainTableView] beginUpdates];
[[self mainTableView] insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationRight]; [[self mainTableView] deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationRight];
[[self mainTableView] endUpdates]; }

 

These are the same commands we did previously, just now we are doing them at the same time.  We begin by telling the table view that we are about to give it multiple commands, via the beginUpdates method.  Then we perform whatever actions we need, and tell the table view we are done via the endUpdates method.  The table view will analyze all of these changes and come up with an animation that looks nice, performing the adds and deletes at the same time.

 

If you run the app right now, each time you hit the Silly button, the first row will slide to the right and then back to the left.

 

The 2 animations do not have to be the same.  Change one to Left, and then see what happens.

 

You may have noticed that we did not change the array.  In this (really silly) case, we didn't need to.  We add 1 row, and remove 1 row.  That means a net change of 0, so the number of items in the array is still correct.  BUT, for more complicated situations, you absolutely would need to change the array.  If you add 5 rows and delete 2 others, then your array needs to become 3 items longer.

 

Reorder Rows

 

Changing the order of items in the table is a common need.  You may have noticed that we have not yet seen the drag widgets on the right side of the table.  Just like the delete buttons, we need to implement a method in order to see them.  Also, they only show up in Edit mode, so our Edit button will be involved.  The necessary method is:

 

- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
toIndexPath:(NSIndexPath *)toIndexPath
{
}

 

Just like before, the mere presence of this method is enough to show the widgets.  Run the app, hit the Edit button, and you will see them.  If you try to reorder rows, it will LOOK like it works, but it really doesn't.  In order to prove that, I need to implement a different method:

 

- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
   NSString *rowContent = [[self contents] objectAtIndex:[indexPath row]];
   NSLog(@"tapped row is: %@", rowContent);
}

 

Run the app, tap on a row, and then you will see this in the console.  Make sure that when you tap A, you see A in the console.  Then edit the table, drag the last row to the top, and hit the Done button.  Now tap on rows and see what happens.  When you tap on E, you should see A in the console.  Tap on A, you'll see B, and so on.

 

Why is that?  Well, it's the same reason as before.  The table view is doing the visual stuff, but the underlying data - the array - was never changed.  We need to handle that ourselves:

 

- (void)tableView:(UITableView *)tableView
moveRowAtIndexPath:(NSIndexPath *)fromIndexPath
toIndexPath:(NSIndexPath *)toIndexPath
{
   NSString *contentsToMove = [[[self contents] objectAtIndex:[fromIndexPath row]] retain];
[[self contents] removeObjectAtIndex:[fromIndexPath row]]; [[self contents] insertObject:contentsToMove atIndex:[toIndexPath row]];
[contentsToMove release]; }

 

We select the item, and retain it because we don't want it to die when we pull it out of the array.  Then we remove it, and insert it back in at the new location.  Then we release it to balance the retain.

 

Final Thoughts

 

If you read back through this tutorial, you will see that "reloadData" does not appear anywhere here.  reloadData is sort of the nuclear option, and generally speaking it is cheating.  Reloading the table also defeats any animations that might otherwise happen.  In this app right now, if you insert an item, you'll get a nice animation showing the new row.  If you delete an item, you'll get a nice animation removing the row.  The remaining rows are shifted up or down in a nice animated fashion.

 

You could go to the above code, replace each insert/delete line with a reload line, and the end result will still be a table showing the correct contents.  So it does actually "work" to do the reload.  But if you watch carefully, there will be a difference.  The new row just pops into existence.  Deleted rows just instantly disappear.  The animations are gone.  Reloading a table doesn't do any animations.  The user experience is not as good, and frankly using reloadData in these situations is Amateur Hour.  It isn't required, as obviously evidenced by the code above.  So go the extra mile, and don't use table reloading as a replacement for actually understanding how these things work.

 

UITableViewDataSource and UITableViewDelegate define a number of methods to further refine these behaviors.  Have a row that you don't want to be deleted?  There's a method for that.  Have a row that you don't want to be moved?  There's a method for that.  Want to limit where a row can be dragged?  There's a method for that, too.  All I have shown here is the bare minimum necessary to enable these features.  For more complicated behaviors, you will need to employ some or all of these other methods.  See the documentation for a full list of what is available.

 

Here is the sample project for this tutorial:

 

BTITableViewModification.zip

UITableView How-To: Part 5 - More About XIB-Based Cells

Part 1 | Part 2 | Part 3 | Part 4 | Part 6

 

(This is really the second half of Part 4, so we'll just continue with the same project)

 

I've got one more key topic to address, then we'll take a look at some alternate techniques.

 

By the end of Part 4, we created a custom view layout using IB, so we've got the visual part down. That really just leaves one more basic characteristic to talk about: actions. In addition to customizing the look of your cell, you can customize the behavior of your cell, and this is most easily done by adding controls such as buttons, switches, and so on. We're just going to do a quick button, but the concept applies to any of the other controls you may wish to use.

 

So, open up CustomTableCell.xib and drop a button smack in the middle:

 

 

With a typical view controller, we'd connect this button to an IBAction. That's what we want to do here as well, but the question is: where is the IBAction? There are 2 basic options, in the cell class or in the view controller class. Where you should put it will depend on the goal you are trying to accomplish. If you put it in the cell class, then it really isn't any different than wiring it up as you would in a view controller. Depending on your goal, this approach can either simplify or complicate your ultimate outcome. But for our purposes here, we're going to put the IBAction in the view controller. So add this declaration to BasicViewController.h:

 

- (IBAction)buttonPressed:(UIButton *)button;

 

(you are probably accustomed to seeing (id)sender for these things. Go ahead and do that if you prefer. I like the type to be correct, and rarely have a need to mix these methods with multiple types of controls)

 

Now we just need to wire it up. You're going to drag from the button to File's Owner like you normally would. The only difference is that this is happening in the cell class instead of the view controller class.

 


 

All that's left is to make the method do something. You can do a simple stub for now to make sure that this works:

- (IBAction)buttonPressed:(UIButton *)button
{
NSLog(@"It worked!");
}

 

Go ahead and run the app, and you should see this message in the console when you hit any button. And now we reach the fun part. As you can see, this method fires when you hit any button. So, how do you know which button?

 

There are a couple of ways to answer this question and, like so many programming challenges, which way to go will depend on your situation. You could use the tag property of the button. This is a common thing to do when you have multiple buttons and would like to distinguish between them. You would need to declare a property in the cell for the button, and then you could assign the tag value in cellForRow like this:

 

[[cell theButton] setTag:[indexPath row]];

 

Then you extract the tag value in the IBAction method:

 

NSLog(@"Pressed button at row: %d", [button tag]);

 

For many tables, this will be an adequate and simple approach. However, if you recall some behaviors from Part 3 regarding multi-section tables, then your spidey sense should be warning you that something may be wrong here. We are using the row parameter to identify the cell, but what happens if there is more than one section? You could have row 0 in the first section, and row 0 in the second section, and row 0 in the third section... Remember that row indexes reset in each section. This is why NSIndexPath provides TWO parameters - section and row - because only one parameter is insufficient to find/describe the location.

 

UIButton doesn't have an NSIndexPath property. If you really wanted to, you could subclass UIButton to add that property. Then the approach above would be the same, except you would provide/extract the indexPath instead of merely the row. I personally tend to view subclassing as a last resort, so I've never bothered doing this. But I suppose I could see some situations where BSTableButton could come in handy. But that isn't why I've brought you here today.

 

No matter what the approach, you have to do some work. Above we've done a little work in cellForRow, and a little work in the IBAction method. (And if you subclassed, you've done a lot of work in even more places) Also think in terms of performance. You are flicking your way through a table, and you want to get in and out of cellForRow as fast as possible. Assigning the tag (or indexPath) is just one more thing that has to be done that is preventing you from leaving the method ASAP. So instead of doing the work for each cell, just do the work when the user actually hits the button. The question is how.

 

In the IBAction, I receive the button itself as a parameter. From there, I would like to wind up with an indexPath. This will allow me to know that the 5th button was pressed, so I can go do something to the 5th item in the array. A quick glance at the UITableView documentation reveals a handful of methods that return an indexPath. For all visible rows. For the selected row (before you get excited, this is only for the blue highlighted row, not for tapping on items in the row). The only one that seems useful in this situation is indexPathForCell:. If I have a cell, the table can tell me where it is located.

 

Ok, so how do I get to the cell from the button. Ah ha! Remember that these are all views, and that they have been arranged into an hierarchy. The button would be a subview of the cell, so since I'm starting with the button, I would need to walk UP the hierarchy. You do that with the superview method.

 

This part can require some learning and trial-and-error, so let's do it the hard way before we skip to the answer. Add this to the IBAction:

 

NSLog(@"superview is: %@", [[button superview] description]);

 

Run it and you should see something like this:

 

superview is: <UITableViewCellContentView: 0x4d31390; frame = (0 0; 320 43); layer = <CALayer: 0x4d313f0>>

 

Hrm, UITableViewCellContentView. We're looking for CustomTableCell, so that's not a match. If you look at the top of the UITableViewCell documentation, you'll find the contentView property mentioned, and then you can go read its description. Basically, what we added the button to was the contentView, not the cell directly. So we need to go higher in the hierarchy:

 

NSLog(@"superview is: %@", [[button superview] description]);
NSLog(@"superview superview is: %@", [[[button superview] superview] description]);

 

Try again:

 

superview is: <UITableViewCellContentView: 0x4d31390; frame = (0 0; 320 43); layer = <CALayer: 0x4d313f0>>
superview superview is: <CustomTableCell: 0x4b3ad70; baseClass = UITableViewCell; frame = (0 264; 320 44); autoresize = W; layer = <CALayer: 0x4b39e20>>

 

Bingo. So, with this particular hierarchy, to go from the button to the cell, we need to go up 2 levels in the view hierarchy. This will depend on your actual view hierarchy, which is why I showed the long steps first. Just keep walking up the hierarchy until you find the cell. (I'm sure you could come up with an algorithm to keep walking, but it's never been worth the effort for me) Now that we know where we're going, our actual line of code is:

 

CustomTableCell *cell = (CustomTableCell *)[[button superview] superview];

 

We have the cell, and can now get the indexPath:

 

NSIndexPath *indexPath = [[self mainTableView] indexPathForCell:cell];
NSLog(@"indexPath is: %@", [indexPath description]);

 

Now you have the exact same location that you would have in cellForRow. So you can access your array the same way, and go manipulate your data or otherwise make something happen as you see fit.

 

Cell Identifiers Suck

 

I mentioned in Part 4 that I don't like the way Apple put the cell identifier into the cell's XIB. About a year ago, I discovered a blog post by Jeff LaMarche that provides a workaround. I won't bother repeating what he has said there, so let's just apply the code with a little twist.

 

Add this to CustomTableCell.h:

 

+ (NSString *)reuseIdentifier;

 

And then add these to CustomTableCell.h:

 

- (NSString *)reuseIdentifier
{
return [[self class] reuseIdentifier];
}

+ (NSString *)reuseIdentifier
{
return NSStringFromClass([self class]);
}

 

It's the same thing Jeff shows, I just took out the constant declaration as mentioned in the comments. This allows for easy copy-pasting into any cell class.

 

You can now blank out the identifier setting in IB, or forget to include it altogether.

 

And Now For Something New

 

There are a variety of debates regarding XIB-based cells. First there are the usual arguments with the "I only use code, IB is the debbil" people. Then there is another layer for use in cells, with people arguing that XIBs are slower than code because you have to go do disk to load the cell. There must have been some truth to this, because Apple added a new class for OS 4 to address it. It's too bad it took so long, because if anything we needed it more in slower-hardware days of OS 2 than we do today, but they felt it was important enough to create, so we should at least give it a look.

 

Now presenting: UINib.

 

Read the documentation and you'll find that it is basically a caching and instantiation class for loading XIBs. You can use it for any kind of XIB loading, but you can be pretty confident that it was developed for table cells. It doesn't wind up making a tremendous difference in terms of code - if anything it adds some - but I'll take Apple at their word that this is faster in terms of performance. I like using UINib as a property, so add this to BasicViewController.h:

 

@interface BasicViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) IBOutlet CustomTableCell *customTableCell;
@property (nonatomic, retain) UINib *customTableCellNib;

 

And then I implement it with a custom getter in the .m file:

 

@@synthesize customTableCellNib = ivCustomTableCellNib;
...

// dealloc
[self setCustomTableCellNib:nil];
...

- (UINib *)customTableCellNib
{
if (ivCustomTableCellNib == nil)
{
ivCustomTableCellNib = [[UINib nibWithNibName:NSStringFromClass([CustomTableCell class]) bundle:nil] retain];
}
return ivCustomTableCellNib;
}

 

I customize the getter so that I don't have to care who creates it or when. The first time it is needed, it will get created.

 

So we've added a property, and we've added a method. What's the payoff? Well, not much really. Here is what we had before:

 

if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];
cell = [self customTableCell];
[self setCustomTableCell:nil];
}

 

This now becomes:

 

if (cell == nil)
{
[[self customTableCellNib] instantiateWithOwner:self options:nil];
cell = [self customTableCell];
[self setCustomTableCell:nil];
}

 

Wow. We changed one line of code. Yay. Well, this is more about performance than it is about code reduction.

 

A Couple Of Tweaks

 

Speaking of performance, there is one thing you want to be careful of in cell design, whether it is in code or via XIB: avoid transparency. With our fancy iPhone 4's and iPad 2's, this is becoming less and less of an issue, but the basic premise remains the same: the device likes opacity a whole lot more than it does transparency.

 

What this specifically means as far as the view items go is to turn on the Opaque toggle, and supply a background color. The Opaque toggle is here:

 

 

When you first turn it on, the items will often default to black. Simply apply a different color:

 

 

Naturally this won't work if your background is not consistent, like a gradient or an image. In those cases, you're pretty much stuck with the transparency.

 

The next thing to be aware of isn't performance related, but it is very visual so you'll want to make sure to catch it. For labels (and some others like image views) you will want to specify a highlight color:

 

 

This is the color that the text will be when the user highlights a row. Generally speaking you will want it to be white, but that's a design decision for you. At some point in the last couple releases of Xcode, and I'm not sure when this started, the default automatically became white. So hopefully you shouldn't have to mess with this too much. But if you're on a slightly older version of Xcode, be aware that the default used to be black. Regardless, if you tap a cell, and don't see nice white text, this is where you need to go to fix it.

 

Holy Crap, This Is Long

 

Now you know why I split this into 2 parts. Head back to the cell's XIB, and we're going to make a slight change. Make the cell taller. You can add more stuff if you want, but for immediate purposes, all I want is something like this:

 

 

Depending on how you set up the label's masks, when you run the app now you should get something like this:

 

 

Well that's no good. What happened here? I made the cell taller, why isn't it displaying that way? Ah, because nobody told the table that the row height should change. So we need to do that.

 

The wrong answer is to use this delegate method:

 

tableView:heightForRowAtIndexPath:

 

This method should ONLY be used if your table will feature more than 1 row height. If you have tall rows and short rows and everything in between, then this method is pretty much your only option. But if your rows are all the same height, then you will slam against this method over and over and over again (seriously, add some logs and watch how much it gets called) just to return the same number each time.

 

Fortunately, there is a better answer: UITableView has a rowHeight property. In fact, we've already utilized it, we just didn't realize it. Open up the BasicViewController.xib, select the table view, then hit Cmd-3. Right there at the top of the inspector: row height of 44.0. So we could simply change this number to match whatever our cell height is, and things will be good.

 

Or will they? Hard-coding numbers is generally something to avoid. And really, we could change the cell design again, and change the height again, and then we just forget to come back and change this setting. Wouldn't it be nice if we could establish a link between our cell height and our row height?

 

We can't quite get there directly, but we sure can take a measurement and use that to define the row height.

 

So first we need a cell. viewDidLoad is a good place to do this, and we are literally going to do the same thing we do in cellForRow to get a cell:

 

[[self customTableCellNib] instantiateWithOwner:self options:nil];

 

From what we learned before, we know that the cell property is now occupied. So we can grab that cell, take a measurement, and drive the table:

 

[[self customTableCellNib] instantiateWithOwner:self options:nil];
[[self mainTableView] setRowHeight:[[self customTableCell] frame].size.height];
[self setCustomTableCell:nil];

 

We get the frame of the cell, then get the height, and give that value to the rowHeight property. Then we clear out the cell property since we're done with it.

 

Try again:

 

 

Muuuuch better. Now you can change the cell's height as much as you want, and the table will always display the correct row height.

 

Thus concludes a couple of lengthy posts on this topic. But don't let that volume scare you away from using XIB-based cells. After you've run through the process a few times, it gets less intimidating. And once you're there, you get to take full advantage of Interface Builder's promise to make view layout easier.

 

TableViewTutorial_Part5.zip

UITableView How-To: Part 4 - XIB-Based Cells

Part 1 | Part 2 | Part 3 | Part 5Part 6

 

Up to this point in the series, the focus has been on basic table and data structures. Now let's take a look at customizing the appearance of the table view.

 

I'm not intending to spend much time talking about standard cells, but there actually are a number of ways to customize cells without needing to subclass. UITableViewCell's initWithStyle: method accepts a parameter for which Apple has provided several standard options. You can show text in several places, and you can add images and accessories. Take a quick glance at the Settings app on your phone; those are standard cells. So before you dive into subclassing cells, make sure you are aware of the built-in options. They can save you a lot of time. This post at Cocoa With Love is definitely worth reading.

 

But, let's assume that those standard configurations are inadequate for your awesome table design. Or perhaps you've seen another app that displays lots of crazy things in a table, and you wonder how it was done. Chances are that the answer will be the same either way: custom table cells. Like most visual things in Cocoa, there is a code-based approach and an Interface Builder-based approach. I'm going to focus on using IB, as the layout is significantly easier, although the setup has some nuances. This approach is based on the Apple sample project called TaggedLocations.

 

Overview

Let's take a quick look at the players involved, because the process to set this up is a tad convoluted:

ViewController.h
ViewController.m
ViewController.xib

TableCell.h
TableCell.m
TableCell.xib

 

In the same way that you would reference a button in your view by creating an IBOutlet, the same thing will be done here. So the view controller will have an IBOutlet for the cell:

@property (nonatomic, retain) IBOutlet TableCell *tableCell;

 

In order to do this without causing any build errors, the cell class needs to exist. But this outlet needs to exist in order to to complete the TableCell.xib, so there is something of a chicken-and-egg situation. The basic steps are:

 

1. Create the view controller class
2. Create the cell class
3. Create the IBOutlet in the view controller
4. Design the cell
5. Rewire the table delegate methods to use the cell

 

So there is a fair amount of back-and-forth between the classes, but after you get used to it, it's not so bad. Let's begin…

 

1. Create a view controller. I'm calling this one BasicViewController, but you can use whatever you want. Don't bother getting too hung up with delegate methods for now.

 

2. Add a new file. This will be a UITableViewCell subclass, and it's not immediately clear how to do this. Just choose Objective-C class, and then select UITableViewCell from the pull-down menu:

 

 

Call it CustomTableCell. This will create the .h and .m file, but we want a XIB file, too. So add a new file for that as well. Select User Interface at the side, and then choose a View XIB. iPhone-vs-iPad doesn't really matter, but I tend to go with iPhone.

 

 

Give it the same CustomTableCell name. You should now have .h, .m, and .xib files for CustomTableCell.

 

3. Now create an IBOutlet for the cell in the view controller. You will need @class in the .h file and #import in the .m class. The highlights of what you should wind up with are:

 

// BasicViewController.h
#import <UIKit/UIKit.h>
@class CustomTableCell;

@interface BasicViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
}

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

 

// BasicViewController.m
#import "CustomTableCell.h"

@implementation BasicViewController

@synthesize mainTableView = ivMainTableView;
@synthesize customTableCell = ivCustomTableCell;

- (void)dealloc
{
[self setMainTableView:nil];
[self setCustomTableCell:nil];
[super dealloc];
}

 

4. Configure the cell. Open up CustomTableCell.xib, as we have some preliminary things to take care of. You should see:

File's Owner
First Responder
View

Select View and delete it. Now go to your Library palette, find the Table View Cell, and drag it to the spot where View was.

 

 

Now we need to change classes. Select File's Owner, and go to the inspector panel for Identity (Cmd-4). Change the class to BasicViewController. Now select the table view cell, and in the inspector change the class to CustomTableCell.

 

 

Now we need to connect the IBOutlet we made earlier. Ctrl-click on File's Owner. Select the customTableCell outlet, and drag that to the CustomTableCell item.

 

 

Hey, wait a second... this isn't my view controller's XIB! You are correct. But the view controller will create this class - it will be the file's owner - so it does make some sense to do this. But don't get carried away. For example, you do see the view outlet, but don't mess with that here as that is being populated in your view controller's XIB. We are doing this for one reason and at this point one reason only: to get access to that cell IBOutlet.

 

There is a lot of customization we could potentially do here, but in the interest of quickly moving along to see this in action, simply drop a couple of a labels onto the cell. We'll come back later to wire everything up.

 

 

5. Configure the table delegate methods. Return to BasicViewController, and for now tell it there are 10 rows. The important change happens in cellForRow:

 

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

CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];
cell = [self customTableCell];
[self setCustomTableCell:nil];
}
return cell;
}

 

For the most part, this is pretty similar to what we've done before. Instead of a UITableViewCell, we're using a CustomTableCell, so we change the class types to reflect that. We're still asking for an available cell, and if one isn't available then we create one. The creation part is different than we've done before, naturally since we aren't just using code this time.

 

The first thing we do is load the XIB file. We indicate which class, and who should own it. The next thing we do is wave our hands and say there is some black magic happening here. Then we assign the cell property to our cell variable. Hrm, what? How did that get there? Well, let's back up and address that black magic.

 

The documentation has this to say about loadNibNamed:owner:options:

During the loading process, this method unarchives each object, initializes it, sets its properties to their configured values, and reestablishes any connections to other objects.

 

For our purposes, the important part is the last bit.

 

Think about what you do with a normal view controller. You place, say, an image view in IB, you create an IBOutlet for it, and then make the connection in IB. So after you instantiate your view controller, what happens? The XIB is loaded, which means an image view is also instantiated, and this image view is then assigned to your IBOutlet/property. When you go to talk to your image view using the property - [[self imageView] setImage:...]; - the image view is there already.

 

What we're doing here is exactly the same, only splitting things up into separate files. Instead of an image view, it's a table cell, and instead of being in the view controller's XIB, it is in a separate XIB. But the act of loading the XIB causes the outlets to be populated, so we end up with the same result. Black magic indeed.

 

So:
1. We load the XIB file
2. The IBOutlet gets populated with the cell
3. We assign that cell to our local cell variable
4. We clear out the property since we don't need to keep it around

 

Go ahead and run the app at this point, and if you've wired everything correctly, you should see:

 

Configure the cell

 

Let's circle back around and finish up the cell. First of all, we skipped a really important step. Recall what the initializer for a standard cell looks like:

 

cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];

 

We pass two parameters: a style, AND a reuse identifier. Now compare to how we just created the cell:

 

[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];

 

Uh oh, no reuse identifier parameter. That's a problem if we want good table scrolling performance (and we do). I wish Apple would have handled this differently, but what they did is imbed the reuse identifier into the XIB file. So open it back up, and select the cell. Then go to the inspector panel again, Cmd-1.

 

 

It will be blank when you first look at it, so you will need to type it in. The important part here, and one unfortunate aspect of Apple's decision to do this, is that whatever you type in here, needs to match what you type in here:

 

static NSString *CellIdentifier = @"CustomCellIdentifier";

 

So you have to make sure the same thing is typed in 2 places, and if you screw up either one then you won't recycle cells. This will hurt scrolling performance. I really wish Apple had gone with more of an initWithNibName:bundle:reuseIdentifier: approach for these cells. Oh well.

 

Let's add some properties to the cell so that we can talk to the labels.

// CustomTableCell.h
@interface CustomTableCell : UITableViewCell
{
}

@property (nonatomic, retain) IBOutlet UILabel *redLabel;
@property (nonatomic, retain) IBOutlet UILabel *greenLabel;

 

// CustomTableCell.m

@implementation CustomTableCell

@synthesize redLabel = ivRedLabel;
@synthesize greenLabel = ivGreenLabel;

....

- (void)dealloc
{
[self setRedLabel:nil];
[self setGreenLabel:nil];
[super dealloc];
}

 

Standard stuff here. Where it gets tricky is actually making the connections in IB. You are probably accustomed to dragging from File's Owner to establish IBOutlet connections. Ah, but remember which class we're dealing with here. File's Owner is the view controller, but we are adding these IBOutlets to the cell. So you can drag from File's Owner all that you want, but you won't be able to create the links. You have to drag from the cell class to the labels:

 

 

Now let's head back to the view controller and put some data in the labels. I'm not going to bother setting up any data; refer to Part 3 for some thoughts on how to arrange your data for this purpose. For now, just drop in something so that you can see different text in each field:

 

   ...

NSUInteger row = [indexPath row];
[[cell redLabel] setText:[NSString stringWithFormat:@"Red %d", row]];
[[cell greenLabel] setText:[NSString stringWithFormat:@"Green %d", row]];

return cell;
}

 

With standard cells, you are talking to [cell textLabel] or [cell detailTextLabel]. Same idea, just using the properties that you've created.

 

Your cell is a blank canvas ready to be customized to your heart's delight. Want 5 labels? Good. Want 10 images? Great. Go nuts.

 

This is turning into a longer post than I thought, and I still have a lot to talk about. So I'm going to split this up into 2 posts. Tune in later for the sequel.

 

TableViewTutorial_Part4.zip

UITableView How-To: Part 3 - Multiple Sections

Part 1 | Part 2 | Part 4 | Part 5Part 6

 

In my experience so far, people seem to have a knack for making multi-section table views harder than they really are. The key to simplifying things is to prepare your data in such a way that pain is removed from your table delegate methods. If your delegate methods are nothing but switch/case statements or a ton of if/else if statements, then you've likely given yourself a pretty good headache.

 

Let's quickly revisit some key elements from Part 1. To determine the number of rows, we did this:

 

NSInteger rows = [[self contentsList] count];

 

...and to get the information to show in the cell, we did this:

 

NSString *contentForThisRow = [[self contentsList] objectAtIndex:[indexPath row]];

 

We count up everything we have, and then we use the row parameter to extract the specific piece of information. That's fine when we have one continuous list, but if you want a sectioned display, then you don't have one continuous list anymore. And this is true regardless of the display type.

 

There are two default display options for table views. Plain:

 

 

And grouped:

 

This is purely visual fluff. You can go back to the exercise in Part 1 if you want and flip it to grouped, and nothing else needs to change. The easiest way to manage this sectioned/grouped appearance is to group your data as well. The long list of 500 names that you have in your address book needs to be broken up into pieces.

 

How you actually get your data broken up is a programming exercise that I'm not going to go into here. There are lots of ways to do it, and the 'correct' approach will depend on your actual data and your specific needs. What I will show here are two different structures that you can use that simplify the delegate methods considerably. They are:

  • An array of arrays
  • A dictionary, and an array of keys

But first we need to introduce a delegate method that we haven't seen or used yet. In Part 1, we answered the question "how many rows in this section". The second half of that question is important, as we never indicated how many sections there are. This is yet another question the table view can ask, and it is:

 

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView

 

This is an optional method, as evidenced by the fact we didn't use it in the previous exercises. But clearly the table works without it, so what gives? As noted in the documentation, the default value is 1. So the tables we've seen so far actually were sectioned table views, just with only 1 section.

 

Just like we did for the number of rows, we should probably base the answer on a calculation. If you have a simple table that will only ever have 2 sections, then by all means go ahead and hard-code a 2. But if the number could change, it needs to be related to your data somehow. More on that in a moment.

 

For now, I'm going to take the code from Part 1, remove a couple of colors, and then set the number of sections to 3.

 

 

Notice that the rows repeat; I have 3 groups of the same thing over and over again. Why is that? Well, let's remember how we collected the data to display:

 

NSString *contentForThisRow = [[self contentsList] objectAtIndex:[indexPath row]];

 

Using the row alone, we pull data from the array. Why does it repeat? Because the row numbering starts over for each section. Section and row numbers look like this:

 

Section 0
Row 0
Row 1
Row 2
Section 1
Row 0
Row 1
Row 2
Section 2
Row 0
Row 1
Row 2

 

Three sections, so the table view asked for something to display in Row 0 three times. And that's exactly what we gave it: the first item in the array, 3 separate times.

 

This is why arranging the data in a particular way is important.

 

How do we find our way around in the table? Well, we've already seen this:

 

[indexPath row]

 

Now we also need to use this:

 

[indexPath section]

 

NSIndexPath actually does a lot more than this, but for most iPhone purposes it is used to describe a section and row location in a table view. In Part 1, we used the row parameter to select an item from the array. We are still going to do that, but we will now use the section parameter to decide which array.

 

Array of Arrays

 

As previously stated, the key is arranging your data in a way to facilitate a sectioned table view. We'll keep the same contentsList array that we had before, but we'll change the contents. Before, it contained only strings. Now, it will contain arrays. Those arrays will contain strings.

 

NSArray *firstSection = [NSArray arrayWithObjects:@"Red", @"Blue", nil];
NSArray *secondSection = [NSArray arrayWithObjects:@"Orange", @"Green", @"Purple", nil];
NSArray *thirdSection = [NSArray arrayWithObject:@"Yellow"];

NSMutableArray *array = [[NSMutableArray alloc] initWithObjects:firstSection, secondSection, thirdSection, nil];
[self setContentsList:array];
[array release], array = nil;

 

Same basic idea as before, but we've added some structure. We need to make adjustments to the delegate methods to account for this new structure. First, our new delegate method for number of sections:

 

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger sections = [[self contentsList] count];

return sections;
}

 

This is the same calculation we started with, but now we are answering a different question. In Part 1, this calculation was for the number of rows. Now it is the number of sections. So far so good. Now we need to define the number of rows. You'll note I set up the arrays so that each one has a different number of objects. This is to help reinforce that these numbers probably shouldn't be hard-coded. You want everything to work whether your array has 5 objects or 500 objects.

 

In Part 1, the number of rows was the number of items in the main array. That is no longer the case. We must first identify which sub-array we're interested in, and then count that sub-array. There was a parameter we ignored before:

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section

 

How many rows are in this section? We've told the table how many sections there will be, and the table will now call this method for each section, passing in the appropriate value. We'll use this to identify which array we want.

 

NSArray *sectionContents = [[self contentsList] objectAtIndex:section];

 

For the first section in the table, I want a reference to the first sub-array in the main array. Second array for the second section, and so on. The number of rows is then the count of this sub-array.

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSArray *sectionContents = [[self contentsList] objectAtIndex:section];
NSInteger rows = [sectionContents count];

return rows;
}

 

So it is the same idea as what we did before, we just have to count a different array each time. It's not horribly complicated, you just have to plan for it.

We use this same concept again to determine what the row contents are. The only difference is that we get to the section value through the indexPath parameter.

 

NSArray *sectionContents = [[self contentsList] objectAtIndex:[indexPath section]];

 

Now we do the same thing we did before, using the row parameter, but using this array instead of the main one.

 

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSArray *sectionContents = [[self contentsList] objectAtIndex:[indexPath section]];
NSString *contentForThisRow = [sectionContents objectAtIndex:[indexPath row]];
...

 

After this, nothing is different than what was done in Part 1. Feed this string into the cell, and you should be good to go. If everything is wired up correctly, then you should see:

 

That's really all there is to it. Once you have this structure in place, you can add or remove as many colors as you want - to/from each section - and you don't have to mess with the delegate methods anymore. We added 1 delegate method, and 1 line of code each to two existing delegate methods (plus the extra stuff for setting up the data) vs. what we had in Part 1. Easy!

 

A dictionary, and an array of keys

 

Again there are many possible ways to structure your data, so I offer this next one merely as another example. But it is handy if you want even more data in your table view, specifically headers. If you look at the address book, you'll see letters for each group of people - A's, B's, etc. - and this data has to be set up somewhere, somehow.

 

Dictionaries store data using keys, typically strings. So you store something by name, and you retrieve something by name. Those names can be easily used as section headers. The problem is that dictionaries do not have order. There is no first object, second object, etc., and tables really like for things to be in order. So in addition to using the dictionary, we will continue to use an array to provide order.

 

Again, how you set up the data is pretty important. So let's start with the basics, we have a dictionary and an array:

 

@interface DictionaryViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *sectionKeys;
@property (nonatomic, retain) NSMutableDictionary *sectionContents;

 

I've kinda shown my hand here with the names. The dictionary will hold the contents of each section, and the array will hold the keys. The contents will be arrays, just like in the previous example. We're just going to access them in a different way. The data is prepared like so:

 

NSMutableArray *keys = [[NSMutableArray alloc] init];
NSMutableDictionary *contents = [[NSMutableDictionary alloc] init];

NSString *colorKey = @"Colors";
NSString *clothingKey = @"Clothing";
NSString *miscKey = @"Misc";

[contents setObject:[NSArray arrayWithObjects:@"Red", @"Blue", nil] forKey:colorKey];
[contents setObject:[NSArray arrayWithObjects:@"Pants", @"Shirt", @"Socks", nil] forKey:clothingKey];
[contents setObject:[NSArray arrayWithObjects:@"Wankle Rotary Engine", nil] forKey:miscKey];

[keys addObject:clothingKey];
[keys addObject:miscKey];
[keys addObject:colorKey];

[self setSectionKeys:keys];
[self setSectionContents:contents];

[keys release], keys = nil;
[contents release], contents = nil;

 

This should look reasonably similar to what we did before. We've added a dictionary, and you add data to a dictionary differently than you do an array, but otherwise it is the same idea. If you're paying attention to details (and as a programmer, you should be) then you'll notice that the order I added the keys is different than the order I added the arrays. I only did this to illustrate that the order of the dictionary doesn't matter, and the order of the array is what will be driving the table.

 

After this, the approach is pretty similar to what we did before. We need to tell the table how many sections:

 

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
NSInteger sections = [[self sectionKeys] count];

return sections;
}

 

Since the dictionary and the array have the same number of objects, I could have counted either one. But typically you'll want to use the array. If I'm testing various arrangements, I will often make the contents the same regardless, and observe differences by messing with the keys. Don't want colors today? Just don't add the key to the array, and nothing else needs to change.

 

Now we need to provide the number of rows. This is the same approach as last time, just going through the dictionary.

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSString *key = [[self sectionKeys] objectAtIndex:section];
NSArray *contents = [[self sectionContents] objectForKey:key];
NSInteger rows = [contents count];

return rows;
}

 

We grab the key using the section parameter, then grab the sub-array using that key. Same thing for the row contents:

 

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *key = [[self sectionKeys] objectAtIndex:[indexPath section]];
NSArray *contents = [[self sectionContents] objectForKey:key];
NSString *contentForThisRow = [contents objectAtIndex:[indexPath row]];
...

 

At this point, we've essentially recreated the first example. But we went this way for a reason, and that reason is section headers. There is another delegate method:

- (NSString *)tableView:(UITableView *)tableView
titleForHeaderInSection:(NSInteger)section
{
NSString *key = [[self sectionKeys] objectAtIndex:section];

return key;
}

 

We've already seen how to grab the key, so we simply do that and use the key as the header.

 

Food For Thought

 

Usually when I see rookie attempts at multi-section tables, there is a lot of code like this:

 

if (section == 0)
{
...
}
else if (section == 1)
{
...
}
...

For a simple structure: Ok, it probably doesn't make a big difference
For a complex structure: No, just no

 

This is paving the way for a modification nightmare. We've only looked at cellForRow so far, but you're going to do the same thing in didSelectRow (we'll get to that later), too. If you decide to rearrange things, you've got to remember everywhere that it is supposed to change, and of course you'll forget, and things will go badly.

 

You'll notice that the code I've posted so far doesn't look anything like this. The logic is provided up front by the structure of the data, so it isn't necessary to complicate the delegate methods. All they have to do is select data, no other decisions are necessary.

 

But let's assume for a moment that there is indeed a reason to further customize in the delegate methods. Let's say that all of the text in the colors section should be red. No problem. But you still don't want to hard-code like this. What happens if tomorrow you decide that colors should be section 5? Then you have to change all of these statements.

 

There isn't really a good option for the array-of-arrays case, so this may be a good vote in favor of the dictionary approach. Rather than hard-coding the section number, I can be flexible according to the section key.

 

if ([key isEqualToString:@"Colors"])
{
// Make them red
}
else
{
// Make them black
}

 

Now you've got a condition that will trigger correctly regardless of the order of the data. And if today you are testing without colors, no problem. Drop the key, and this condition will never trigger.

 

Once you get comfortable with the basic concepts here, you may want to take a look at a post I made a long time ago: Taming Table Views. There, I show the structure of a custom model class that I use all of the time with sectioned table views. This would be used with the array-of-arrays approach, but instead -of-arrays, it would be -of-DisplaySections. It has a field for the header, a field for the letter index, a field for behind-the-scenes stuff if needed, and it has an array property for the contents. A couple months after I wrote that, I discovered that Apple has a similar class (actually a protocol) for working with CoreData stuff called NSFetchedResultsSectionInfo.

 

One last comment regarding searching. If all you do is take the code in these samples and apply them to your project from Part 2, then searching will not work. Keep in mind the way we've changed the structure. It used to be an array of strings. It is now an array of arrays of strings. So you will have to adapt the search routine to this new structure. It is along the same lines as what we've done above in the delegate methods, so you should be able to figure it out.

 

TableViewTutorial_Part3.zip

UITableView How-To: Part 2 - Search

Part 1 | Part 3 | Part 4 | Part 5Part 6

 

If you have a lengthy list of data, you should provide the ability for your users to search through that data. Starting with OS 3.0, Apple made integrating search into table views easy enough that there really isn't a good reason not to include it.

 

This post is based heavily, if not entirely, on Apple's sample project called TableSearch.

 

UISearchDisplayController

 

The search UI that Apple provides is basically a table view with a search bar. They provide some pretty animations, such as sliding the search bar up to cover up the navigation bar (handy for maximizing available space, especially in landscape). The search table view is overlaid onto your existing view, so both your original table and the search table need data. The principles of delegate and data source as discussed in the last post still apply.

 

In addition to creating the controller itself, Apple also gave UIViewController a searchDisplayController property, which makes it easier to access. This obviously isn't populated by default, but be aware that it exists.

 

 

Data

 

Say you have a list of 10 items. The user performs a search, and only 2 items meet the criteria. Now think back to our datasource and delegate methods. We are answering a lot of questions about the table: how many sections, how many rows, what cell should be displayed (and what should the cell contain)? We still have to answer those questions, only now there are two tables involved. Even if there was only a single table, something would still have to be done with the data. There are two basic approaches you can take: 1) Remove items that don't match, or 2) Create a whole new list containing only items that do match. If you go with #1, you need some way of restoring the full list. So really, in either case, you are talking about 2 sources of data: the full list, and the search results. Since a separate table is involved for the search results, #2 probably makes more sense, and is what Apple shows in their demo.

 

Step Up To The Bar

 

We'll start with the easiest part of this project. Open up the view controller XIB in Interface Builder, and make sure you can see the table view. Find the Search Bar And Search Display Controller item in the library.

 

 

Do note that this is a separate choice from the standalone search bar. You can certainly roll your own solution using just the bar, but the controller is what makes the work relatively easy. So be sure to grab the one with the little orange circle.

 

In order to get the search bar to scroll with the table, we are going to add it as the table's header. Grab the library item, and drag to the upper portion of the table view. You should see a blue highlight, which is your confirmation that you will get the header.

 

 

A number of things happen automatically when you do this. Let's take a quick look at the object list:

 

 

Even though we didn't directly place it there, the Search Display Controller has been added. If we inspect the connections...

 

 

…we see a whole host of additions. The searchDisplayController property on the UIViewController has been populated. The necessary delegate and datasource connections have been made for the search controller, and the delegate has also been specified for the search bar itself. Not bad for one drag-n-drop operation. We are now done with IB, the rest is handled in code.

 

Changes to .h

 

Here is the end result, then I'll explain what's going on.

 

//  SampleViewController.h

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate, UISearchDisplayDelegate, UISearchBarDelegate>
{
}

@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) NSMutableArray *contentsList;
@property (nonatomic, retain) NSMutableArray *searchResults;
@property (nonatomic, copy) NSString *savedSearchTerm;

- (void)handleSearchForTerm:(NSString *)searchTerm;

@end

 

First, we conform to the UISearchDisplayDelegate and UISearchBarDelegate protocols. Same idea as what we did for the table view previously.

 

Next we declare a couple of new instance variables and properties. The searchResults array will hold items that match the search criteria. The savedSearchTerm is something that Apple shows in their sample, and they use it to restore the search when returning to this screen.

 

Finally, we declare a method that will do the grunt work of searching through the data.

 

Changes to .m

 

We'll start at the top and work our way down. First, synthesize properties and handle memory management duties.

 

@implementation SampleViewController

@synthesize mainTableView = ivMainTableView;
@synthesize contentsList = ivContentsList;
@synthesize searchResults = ivSearchResults;
@synthesize savedSearchTerm = ivSavedSearchTerm;

- (void)dealloc
{
[self setMainTableView:nil];
[self setContentsList:nil];
[self setSearchResults:nil];
[self setSavedSearchTerm:nil];

[super dealloc];
}

- (void)viewDidUnload
{
[super viewDidUnload];

// Save the state of the search UI so that it can be restored if the view is re-created.
[self setSavedSearchTerm:[[[self searchDisplayController] searchBar] text]];

[self setSearchResults:nil];
}

 

That last bit is from Apple's sample. The counterpart is here:

 

- (void)viewDidLoad
{
[super viewDidLoad];

...

// Restore search term
if ([self savedSearchTerm])
{
[[[self searchDisplayController] searchBar] setText:[self savedSearchTerm]];
}
}

 

Saving/restoring the search criteria, nothing fancy. Then we get to the search routine itself:

 

- (void)handleSearchForTerm:(NSString *)searchTerm
{
[self setSavedSearchTerm:searchTerm];

if ([self searchResults] == nil)
{
NSMutableArray *array = [[NSMutableArray alloc] init];
[self setSearchResults:array];
[array release], array = nil;
}

[[self searchResults] removeAllObjects];

if ([[self savedSearchTerm] length] != 0)
{
for (NSString *currentString in [self contentsList])
{
if ([currentString rangeOfString:searchTerm options:NSCaseInsensitiveSearch].location != NSNotFound)
{
[[self searchResults] addObject:currentString];
}
}
}
}

 

First we store the search term. Then we lazily create the searchResults array if needed. Next we clear out any previous search results. Then we loop through our main data, find any matching items, and add them to the searchResults array.

 

This method could vary greatly depending on what kind of data you are working with, and how it is arranged. This particular implementation is based on an example in the Mark/LaMarche book.

 

Now we get to the fun part: the table datasource and delegate methods. We've discussed a bit already about the need to have 2 separate data lists, and pointed out that we will be dealing with 2 separate tables. However, we only have one set of delegate methods here in the controller. We could use a completely separate object, as mentioned in the previous post, but that's not really necessary. So, we need some way of determining which set of data to use. You might get away with using some kind of flag, say BOOL isCurrentlySearching or something along those lines. I have fought enough battles with the search display controller in attempting to do other tasks to know that this approach won't work. Thus, we'll go with the way Apple's sample shows, and that is to make decisions based on which table is asking for information. In hindsight, this is a really obvious approach, but I'm not always on the ball as quickly as I should be.

 

First, the number of rows:

 

- (NSInteger)tableView:(UITableView *)tableView
numberOfRowsInSection:(NSInteger)section
{
NSInteger rows;

if (tableView == [[self searchDisplayController] searchResultsTableView])
rows = [[self searchResults] count];
else
rows = [[self contentsList] count];

return rows;
}

 

The important thing to realize here is that all of these table view delegate methods include the calling table view itself as a parameter. This allows you the means to identify which table is making the request. If you had a reason to design a view with multiple tables, this is exactly what you would do. If the table asking for info is the search table, we provide an answer based on the search list, otherwise we use our main list.

 

We do the exact same thing when providing a cell:

- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSInteger row = [indexPath row];
NSString *contentForThisRow = nil;

if (tableView == [[self searchDisplayController] searchResultsTableView])
contentForThisRow = [[self searchResults] objectAtIndex:row];
else
contentForThisRow = [[self contentsList] objectAtIndex:row];

static NSString *CellIdentifier = @"CellIdentifier";

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

[[cell textLabel] setText:contentForThisRow];

return cell;
}

 

Constructing the cell itself doesn't need to change just because we're using a search table. The key thing is to make sure we're grabbing the right piece of information to populate that cell. "Green" might be at row 4 in our main list, but it could be at row 2 in the search results. Once again, we make a decision based on which table is asking, and grab data from the appropriate list.

 

I'm not really going into cell selection yet, but you would need to do something similar in tableView:didSelectRowAtIndexPath:. The index path can, and most likely will, be different depending on which table is being shown.

 

At last, we reach the part that makes this all work. We've been dealing with table delegate methods up to this point, but the search controller has delegate methods of its own. So we'll utilize a couple of those to make the actual search happen. First, to begin the search:

 

- (BOOL)searchDisplayController:(UISearchDisplayController *)controller 
shouldReloadTableForSearchString:(NSString *)searchString
{
[self handleSearchForTerm:searchString];

return YES;
}

 

And that's it. The documentation notes this is an optional method, and NOT implementing it will cause the search table to reload as the search term changes. So the only reason we're doing this is to define what logic should be performed in response to the search string. And this next part is also optional, but if you want to do any cleanup after the search, this is where you could do it:

 

- (void)searchDisplayControllerWillEndSearch:(UISearchDisplayController *)controller
{
[self setSavedSearchTerm:nil];

[[self mainTableView] reloadData];
}

 

We no longer need the search term to be saved, so get rid of it. And maybe it is appropriate to refresh the main table view.

 

That's pretty much all there is to it at a basic level. Obviously this is a simple app so far, but the search display controller handles a lot duties on its own. Here is the sample project for everything so far:

 

TableViewTutorial_Part2.zip

UITableView How-To: Part 1 - View Controller Setup

Part 2 | Part 3 | Part 4 | Part 5 | Part 6

 

A while back, 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 property, like this:

 

#import <UIKit/UIKit.h>

@interface SampleViewController : UIViewController <UITableViewDataSource, UITableViewDelegate>
{
}

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

@end

 

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>
{
}

@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 = ivMainTableView;
@synthesize contentsList = ivContentsList;

 

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__);

   [self setMainTableView:nil];
   [self setContentsList: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. Add subviews, 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