A handful of predefined cell styles have been available to developers since iOS 3. They are convenient and very useful for prototyping, but in many situations you really need a custom solution tailored to the needs of the project you are working on. In this tutorial, I will show you how to customize table view cells by using static and prototype cells, and by subclassing UITableViewCell
.
Anatomy of a Table View Cell
Even though UITableViewCell
directly inherits from UIView
, its anatomy is more complex than you might expect. If you plan on subclassing UITableViewCell
, it is necessary to understand the anatomy of a table view cell. At its core, a table view cell is nothing more than a view with several subviews, a background and selected background view, a content view, and several other more exotic subviews, such as the accessory view on the right. Let’s take a look at the various subviews.
Content View
As its name implies, the content view contains the cell’s content. Depending on the cell’s style, this can include one or two labels and an image view. As the documentation emphasizes, it is important to add custom subviews to the cell’s content view because it ensures that the cell’s subviews are properly positioned, resized, and animated when the cell’s properties change. In other words, a table view cell expects its contents to be in its content view. The same is true for a collection view cell, which is virtually identical to a table view cell in terms of view hierarchy.
Accessory View
The accessory view of a table view cell can be any type of view. You are probably already familiar with the disclosure and detail disclosure indicator views. An accessory view, however, can also be a button, slider, or stepper control. Take a look at the Settings application on an iOS device to get an idea of what some of the possibilities are. Keep in mind that the space of an accessory view is limited for a standard table view cell. This means that not every view can or should be used as an accessory view.
Editing and Reordering Control
The editing control is another subview of a table view cell that slides into and out of view when the table view’s editing mode changes. When a table view enters into editing mode, the table view’s data source is asked which table view cells are editable by sending it a message of tableView:canEditRowAtIndexPath:
for each cell currently visible. Editable table view cells are told to enter into editing mode, which shows the editing control on the left and, if applicable, the reordering control on the right. A table view cell in editing mode hides its accessory view to make room for the reordering control and the delete confirmation button that appears on the right when a row is marked for deletion.
Background Views
The background and selected background views are positioned behind all other subviews of a table view cell. In addition to these background views, the UITableViewCell
class also defines a background view (multipleSelectionBackgroundView
) that is used for table views supporting multiple selection.
Predefined Styles
In this tutorial, I won’t talk much about the predefined table view cell styles. The main focus of this tutorial is to show you how you can customize table view cells in such a way that is not possible with the predefined table view cell styles. Keep in mind, however, that these predefined cell styles are quite powerful in customizing a table view cell. The UITableViewCell
class exposes primary (textLabel
) and secondary (detailTextLabel
) label as well as the cell’s image view (imageView
) on the left. These properties offer direct access to the cell’s subviews, which means that you can directly modify a label’s attributes, such as its font and text color. The same is true for the cell’s image view.
Despite the flexibility offered by the predefined cell styles, it is not recommended to reposition the various subviews. If you are looking for more control in terms of cell layout, then you need to take a different route.
Option 1: Static Cells
A table view populated with static cells is by far the simplest implementation of a table view from a developer’s perspective. As the name implies, a table view with static cells is static, which means that the number of rows and sections is defined at compile time, not runtime. However, it does not mean that the cell’s contents cannot be modified at runtime. Let me show you how all this works.
Create a new project in Xcode by selecting the Single View Application template, name it Static (figure 1), and enable storyboards and Automatic Reference Counting (ARC). At the time of writing, static cells are only available in combination with storyboards.
Static cells can only be used in conjunction with a UITableViewController
, which means that we need to change the superclass of the existing view controller to UITableViewController
. Before we take a look at the storyboard, add three outlets as shown below. This will enable us to modify the contents of the static cells that we will create in the storyboard.
#import <UIKit/UIKit.h> @interface MTViewController : UITableViewController @property (weak, nonatomic) IBOutlet UILabel *firstLabel; @property (weak, nonatomic) IBOutlet UILabel *secondLabel; @property (weak, nonatomic) IBOutlet UILabel *thirdLabel; @end
Open the main storyboard, select the view controller, and delete it. Drag a table view controller from the Object Library and change its class to MTViewController
in the Identity Inspector (figure 2). Select the view controller’s table view and set its Content attribute to Static Cells in the Attributes Inspector (figure 3). Without having to implement the UITableViewDataSource
protocol, the table view will be laid out as defined in the storyboard.
You can test this by adding several labels to the static cells and connecting the three outlets that we defined in MTViewController.h a few moments ago (figure 4). As I mentioned earlier, the contents of the labels can be set at runtime. Take a look at the implementation of the view controller’s viewDidLoad
below.
- (void)viewDidLoad { [super viewDidLoad]; // Configure Labels [self.firstLabel setText:@"First Label"]; [self.secondLabel setText:@"Second Label"]; [self.thirdLabel setText:@"Third Label"]; }
Build and run the application in the iOS Simulator to see the result. Static cells are quite powerful especially for prototyping applications. They are quite useful when the layout of a table view doesn’t change, such as in an application’s settings or about view. In addition to specifying the number of rows and sections of a table view, you can also insert custom section headers and footers.
Option 2: Prototype Cells
Another great benefit of using storyboards is the prototype cell, which was introduced in conjunction with storyboards in iOS 5. Prototype cells are templates that are dynamically populated. Each prototype cell is identified by a reuse identifier through which the prototype cell can be referenced in code. Let’s take a look at another example.
Create a new Xcode project based on the Single View Application template. Name the project Prototype and enable storyboards and ARC for the project (figure 5). As we did in the previous example, change the subclass of the view controller (MTViewController
) to UITableViewController
. There is no need to declare outlets in this example.
#import <UIKit/UIKit.h> @interface MTViewController : UITableViewController @end
As we did in the previous example, delete the existing view controller in the main storyboard and drag a table view controller from the Object Library. Change the class of the new table view controller to MTViewController
in the Identity Inspector.
As I mentioned earlier, each prototype cell has a reuse identifier through which it can be referenced. Select the prototype cell in the table view and set its Identifier to CellIdentifier as shown in the figure below (figure 6).
As with static cells, you can add subviews to the content view of the prototype cell. There is no need to specify the number of rows and sections as we did in the previous project. When using prototype cells, it is required to implement the table view data source protocol (UITableViewDataSource
). You might be wondering how we reference the subviews of the prototype cell’s content view? There are two options. The easiest option is to give each view a tag and ask the cell’s content view for the subview with a particular tag. Let’s see how this works.
Add a label to the prototype cell and set its tag to 10 in the Attributes Inspector (figure 7). In the MTViewController
class, we implement the table view data source protocol as shown below. The cell reuse identifier is declared as a static string constant.
static NSString *CellIdentifier = @"CellIdentifier";
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 5; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 5; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; // Configure Cell UILabel *label = (UILabel *)[cell.contentView viewWithTag:10]; [label setText:[NSString stringWithFormat:@"Row %i in Section %i", [indexPath row], [indexPath section]]]; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
Run the application in the iOS Simulator to see the result. Even though it seems easy to work with prototype cells by using tags to identify subviews, it quickly becomes inconvenient when the cell’s layout is more complex. A better approach is to use a UITableViewCell
subclass. Create a new Objective-C class, name it MTTableViewCell
, and make it a subclass of UITableViewCell
. Open the header file of the new class and create an outlet of type UILabel
named mainLabel
.
#import <UIKit/UIKit.h> @interface MTTableViewCell : UITableViewCell @property (weak, nonatomic) IBOutlet UILabel *mainLabel; @end
Before we can use our subclass, we need to make some changes to the main storyboard. Open the main storyboard, select the prototype cell, and then set its class to MTTableViewCell
in the Identity Inspector (figure 8). Open the Connections Inspector and connect the mainLabel
outlet with the label that we added to the prototype cell (figure 9).
The changes we made in the storyboard allow us to refactor the tableView:cellForRowAtIndexPath:
as shown below. Don’t forget to import the header file of the MTTableViewCell
class. I hope you agree that this change makes our code more readable and maintainable.
#import "MTTableViewCell.h"
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MTTableViewCell *cell = (MTTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier]; // Configure Cell [cell.mainLabel setText:[NSString stringWithFormat:@"Row %i in Section %i", [indexPath row], [indexPath section]]]; return cell; }
Run the application in the iOS Simulator. Prototype cells are a wonderful component of storyboards. They make the customization of table view cells incredibly easy with little effort.
Option 3: Subclassing
In the previous example, we created a custom UITableViewCell
subclass. However, we didn’t really leverage the power of subclassing. Instead, we relied on the versatility of prototype cells. In the third and last option, I show you how to create a custom UITableViewCell
subclass without using prototype cells. There are several strategies for creating a UITableViewCell
subclass and the one I will show you in the following example is by no means the only way. With this example, I want to illustrate in what ways subclassing differs from the first two options in which we made use of Interface Builder and storyboards.
Create a new Xcode project based on the Single View Application template, name it Subclass, and enable ARC for the new project. Make sure that the checkbox labeled Use Storyboards is not checked (figure 10).
As we did in the two previous examples, start by changing the view controller’s (MTViewController
) superclass to UITableViewController
. Open the view controller’s XIB file, delete the view controller’s view, and drag a table view from the Object Library. Select the table view and set its dataSource
and delegate
outlets to the File’s Owner, that is, the view controller. Select the File’s Owner and set its view
outlet to the table view (figure 11).
#import <UIKit/UIKit.h> @interface MTViewController : UITableViewController @end
Before we create a custom subclass of UITableViewCell
, let’s first implement the table view data source protocol to make sure that everything works as expected. As we did earlier, it is good practice to declare the cell reuse identifier as a static string constant. To make cell reuse (and initialization) easier, we send the table view a message of registerClass:forCellReuseIdentifier:
and pass a class name and the cell reuse identifier as the first and second parameter. This gives the table view all the information it needs to instantiate new cells whenever no reusable cells are available. What does this gain us? It means that we never explicitly have to instantiate a cell. The table view takes care of this for us. All we need to do is ask the table view to dequeue a cell for us. If a reusable cell is available, the table view returns one to us. If no cells are available for reuse, the table view automatically creates one behind the scenes. A good place to register a class for cell reuse is in the view controller’s viewDidLoad
method (see below).
static NSString *CellIdentifier = @"CellIdentifier";
- (void)viewDidLoad { [super viewDidLoad]; // Register Class for Cell Reuse Identifier [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:CellIdentifier]; }
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return 5; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return 5; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; // Configure Cell [cell.textLabel setText:[NSString stringWithFormat:@"Row %i in Section %i", [indexPath row], [indexPath section]]]; return cell; } - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { return NO; } - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { return NO; }
The subclass that we are about to create is pretty simple. My goal is to show you what happens under the hood and what is required to create a UITableViewCell
subclass, as opposed to using static or prototype cells. Create a new Objective-C class, name it MTTableViewCell, and make it a subclass of UITableViewCell
. Open the class’s header file and add a public property of type UILabel
with a name of mainLabel
.
#import <UIKit/UIKit.h> @interface MTTableViewCell : UITableViewCell @property (strong, nonatomic) UILabel *mainLabel; @end
As you can see below, the implementation of MTTableViewCell
is not complicated. All we do is override the superclass’ initWithStyle:reuseIdentifier:
method. This method is invoked by the table view when it instantiates a class for us. The downside of giving the table view permission to instantiate cells is that you cannot specify the first argument of this method, the cell’s style. You can read more about this on Stack Overflow.
In initWithStyle:reuseIdentifier:
, we initialize the main label, configure it, and add it to the cell’s content view. As I explained in the introduction, the latter is very important if you want the custom cell to behave as a regular table view cell.
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { // Helpers CGSize size = self.contentView.frame.size; // Initialize Main Label self.mainLabel = [[UILabel alloc] initWithFrame:CGRectMake(8.0, 8.0, size.width - 16.0, size.height - 16.0)]; // Configure Main Label [self.mainLabel setFont:[UIFont boldSystemFontOfSize:24.0]]; [self.mainLabel setTextAlignment:NSTextAlignmentCenter]; [self.mainLabel setTextColor:[UIColor orangeColor]]; [self.mainLabel setAutoresizingMask:(UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight)]; // Add Main Label to Content View [self.contentView addSubview:self.mainLabel]; } return self; }
To put our new class to use, import its header file in MTViewController.m, update the view controller’s viewDidLoad
method, and amend the tableView:cellForRowAtIndexPath:
method of the table view data source protocol as shown below.
#import "MTTableViewCell.h"
- (void)viewDidLoad { [super viewDidLoad]; // Register Class for Cell Reuse Identifier [self.tableView registerClass:[MTTableViewCell class] forCellReuseIdentifier:CellIdentifier]; }
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { MTTableViewCell *cell = (MTTableViewCell *)[tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath]; // Configure Cell [cell.mainLabel setText:[NSString stringWithFormat:@"Row %i in Section %i", [indexPath row], [indexPath section]]]; return cell; }
Subclassing UITableViewCell
is a much more involved topic than what I discussed in this tutorial. If you want me to write more about this topic, then let me know in the comments below. Don’t forget to run the application in the iOS Simulator to see the subclass in action.
Conclusion
What are the advantages of using a custom subclass as opposed to using prototype cells? The simple answer is flexibility and control. Despite their usefulness, prototype cells have their limits. The main hurdle that many developers face when subclassing UITableViewCell
is the fact that it is tedious. Writing user interface code is tedious and few people – if any – enjoy it. Apple created Interface Builder with good reason. It is also possible to create custom table view cells using Interface Builder and load the XIB file at runtime. I usually create complex table view cells in Interface Builder and translate the design to code when I am happy with the result. This trick saves you a lot of time.
Whether you should use Interface Builder to create custom table view cells or design custom UITableViewCell
subclasses from scratch really depends on the situation and your preference. It is clear, however, that Interface Builder has become more powerful and the introduction of Xcode 4 meant another great leap forward – despite the early bugs and problems it suffered from.
Static cells seem very nice at first glance, but you will quickly run into limitations. However, you can’t deny that it is a very fast way to prototype an application.