Back to Basics: Simple UITableViews
Following up on my previous post in this series, I’m going to continue talking about beginner topics that I and many other developers take for granted. So for this entry in my “Back To Basics” series I’d like to talk about UITableViews, and how to simply and easily construct one without convoluted or confusing code.
This topic in particular is something I’ve struggled over in the past and never managed to find a clear example for how to get started. Certainly there’s a lot of examples to show how to construct a table view, how to create a datasource for it, and the basics for how to construct cells. But hardly anyone tells you how to easily and conveniently construct a menu of options without going down a maze of twisty passages.
So today I’ll show you how you can use simple “typedef” structures to describe and control a simple menu of options.
Basics of UITableView datasources
Before we get started I wanted to go over the UITableViews themselves. Essentially they’re broken down into three parts:
- The tableview itself;
- The datasource which supplies the tableview with information about the sections and rows that should be shown in the tableview;
- The delegate which responds to user interactions, such as selecting an individual cell.
There are many reasons why they’re broken down into those parts, but the big reason is this: performance.
Table views are great because of the sheer speed at which you can scroll through one, even on a tiny device such as an iPhone. It’s trivial to even zoom through a table containing 10,000 rows, and this is because the table only ever constructs views for the cells that happen to be visible at a given moment. If only 10 rows can be visible at a time, that’s a much smaller amount of memory and CPU needed to construct and paint those views on the screen.
The other performance benefit you get is the views for individual cells are reused, meaning you only have to (theoretically) construct them once, and all subsequent cells simply change the values for each cell.
Due to all of that, the datasource is interrogated by the table, asking questions like this:
- How many sections are in the table?
- How many rows are in section X?
- What’s the title of section X?
- Give me a UITableViewCell for row Y in section X
And so on. Typically your delegate and datasource is the same object, so you’ll just have a few methods that answer those questions.
Easily displaying a menu of options
Recently on an update to my oldest iOS application, aCookie Fortune, I wanted to show a menu of options. Some simple buttons, a multiple-choice selection, and a few toggle buttons. Nothing major, but I didn’t want to make things overly complicated. And more to the point, I wanted to build the settings menu as quickly as possible because I don’t have a lot of time in my day, especially for an app that generates as little revenue as this one does.
So when I wanted to have an easily configurable tableview that allowed me to quickly change the order of sections, rows, intermix cell types and accessory views easily, I turned to my good old friend “enum”.
You see, I use enums to control the order of sections, the order of rows, and so forth. If you’re not familiar with an enum, it’s a feature of C that lets you create an enumerated list of variables easily.
Keeping track of sections
In order to define the order of my sections, and the number of visible sections, all I do is declare the following:
enum {
SettingsSectionSettings,
SettingsSectionCategories,
CountSettingsSections
};
This simply describes 3 variables, each with a value 0, 1 and 2. But when the tableview asks my datasource how many sections are in my table, all I have to do is this:
- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView {
return CountSettingsSections;
}
Even though I only plan to show two sections in my table, I declare a third enum constant that is used to show how many sections will be displayed. If I later choose to add an extra section in the middle that will push the “CountSettingsSections” key down, resulting in it having a higher number. This conveniently keeps track of the number of sections without me having to maintain a separate counter.
Number of rows per section
Another thing I need to tell my tableview about is how many rows will be in each section. Similar to the listing of sections, the listing of rows in the first section is controlled by yet another enum listing.
enum {
SettingsSectionSettingsRowInBed,
SettingsSectionSettingsRowSharingOptions,
CountSettingsSectionSettingsRows,
};
The second section, the listing of categories, will be controlled directly by an array of the available categories. So we simply return the appropriate value based on which section number the tableview is asking the datasource about.
- (NSInteger)tableView:(UITableView*)tableView
numberOfRowsInSection:(NSInteger)section {
switch (section) {
case SettingsSectionSettings:
return CountSettingsSectionSettingsRows;
case SettingsSectionCategories:
return [allCategories count];
default:
return 0;
}
}
Notice in the tableView:numberOfRowsInSection: method we use a switch statement to test which section we’re being asked about? Conveniently enough we’re able to use the constants defined in our first enum which lists the sections we’ll use. The handy thing is that if you want to reorganize your sections, all you have to do is reorganize the associated constants in your enum and your section numbers will all update accordingly.
Section titles
There isn’t much difference between returning the number of rows in a section, or the titles for the sections themselves. Again notice that we’re using the section enum to evaluate which section we’re being asked about.
- (NSString*)tableView:(UITableView*)tableView
titleForHeaderInSection:(NSInteger)section {
switch (section) {
case SettingsSectionSettings:
return NSLocalizedString(@"Sharing settings", nil);
case SettingsSectionCategories:
return NSLocalizedString(@"Fortune categories", nil);
default:
return nil;
}
}
Note: You may notice that I’m not simply returning the plain strings, but am instead making them localizable. Do yourself a favour and just do that. Trust me.
Cell contents
Rendering the cell contents is fairly straight-forward, but since there’s typically more code involved in populating your cells I prefer to break those out into separate methods.
- (UITableViewCell *)tableView:(UITableView*)tableView
cellForRowAtIndexPath:(NSIndexPath*)indexPath {
static NSString *CellIdentifier = @"CellDefault";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1
reuseIdentifier:CellIdentifier] autorelease];
}
switch (indexPath.section) {
case SettingsSectionSettings:
[self configureSettingsCell:cell atRow:indexPath.row];
break;
case SettingsSectionCategories:
[self configureCategoriesCell:cell atRow:indexPath.row];
break;
default:
break;
}
return cell;
}
This block of code sets up a common cell, potentially reusing it for performance reasons, and then uses our section enum to determine which section the row we’re being asked to provide lives in. Then depending on section we invoke a different method, supplying the row number that we need to return.
Wrapping up
It’s really that simple. You can create fairly sophisticated interfaces, or part of an interface, using nothing but enums and switch statements. For more involved interfaces you’ll want to mature into using Core Data or your own private data structures to provide your table views with information. But even in complicated applications there are instances where “Simple is better”.