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.
Let's take a quick look at the players involved, because the process to set this up is a tad convoluted:
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:
@interface BasicViewController : UIViewController <UITableViewDelegate, UITableViewDataSource>
@property (nonatomic, retain) IBOutlet UITableView *mainTableView;
@property (nonatomic, retain) IBOutlet CustomTableCell *customTableCell;
@synthesize mainTableView = ivMainTableView;
@synthesize customTableCell = ivCustomTableCell;
4. Configure the cell. Open up CustomTableCell.xib, as we have some preliminary things to take care of. You should see:
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
static NSString *CellIdentifier = @"CellIdentifier";
CustomTableCell *cell = (CustomTableCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
[[NSBundle mainBundle] loadNibNamed:@"CustomTableCell" owner:self options:nil];
cell = [self customTableCell];
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.
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.
@interface CustomTableCell : UITableViewCell
@property (nonatomic, retain) IBOutlet UILabel *redLabel;
@property (nonatomic, retain) IBOutlet UILabel *greenLabel;
@synthesize redLabel = ivRedLabel;
@synthesize greenLabel = ivGreenLabel;
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]];
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.