Wednesday, July 11, 2012

UITableView Tutorial

UITableView is one of the most common types of views used in iOS applications. Let’s go through a tutorial of the most regularly used features of table views.
There are two main types of UITableView. The first is a “plain” table view, which looks like this:
The second is a “grouped” table view, which looks like this:

Both are commonly seen in iOS apps. Grouped table views are often seen in “settings” sections of apps, or on screens where users are inputting or editing information.
In this post we’ll be going through a sample app that uses UITableView. The code is available here.
Data
There are lots of different types of data you might be representing in your table. Maybe it’s a list of countries. Or you might be using the table more for navigation, by giving the user a list of screens they can choose to go to.
iOS keeps it pretty open-ended when it comes to how your data is represented. A UITableView is setup to support a list of rows, optionally split into sections with header names. If you don’t want to show the rows in sections, you would have one unnamed section.
This can be a bit confusing at first, as you might expect to be able to just give the table view an array of rows, and be done with it. Instead, as is common with iOS development, the delegate design pattern is used.
It may be annoying at first, but it actually provides a lot of flexibility for how you choose to represent your underlying data model. You simply define specific methods to use in order to get table information, and iOS can call them when it needs to know something like the number of rows in a section, or the content of a particular row. We’ll get into specifics below.
One thing that makes UITableView unique is that it has two different types of delegates: UITableViewDataSource and UITableViewDelegate. This is fairly unusual in iOS. Usually an object will only have one delegate at most. This can be confusing at first (I know it was for me was I was first learning iOS).
The way to think about these two delegates is that they’re split based on functionality: for the most part, UITableViewDataSource relates to the actual data being shown in your table, whereas UITableViewDelegate deals more with the appearance and UI of the table.
To help differentiate them, here are the types of things each delegate looks for:
UITableViewDataSource:
  • Number of sections in the table
  • Number of rows in a particular section
  • Header and footer titles
  • Being told when rows are reordered, inserted and deleted
  • The particular cell at a given location in the table
UITableViewDelegate:
  • The height of a row at a given location in the table
  • Handling of selection of a row
  • Custom header and footer views for sections in the table
  • Responding to row editing
There’s some overlap between the two. For example, if you allow users to insert a new row, you may want to be notified of that action occurring (through UITableViewDelegate) and also will need to update your underlying data model (through UITableViewData Source).
There are certain delegate methods that are used over and over when working with table views, and over time you’ll probably start to forget whether they live in UITableViewDataSource or UITableViewDelegate. When you find yourself needing one of the less common methods, it can be hard to remember which delegate they belong to, so it’s a good idea to check the documentation for both if you’re not sure.
The class that implements the delegate methods is almost always the view controller that owns the table view. What should that class be? Most view controllers are usually subclasses of UIViewController, but iOS also provides a UITableViewController.
UITableViewController only provides a few features on top of UIViewController:
  • UITableViewController has a tableView property built-in that points to its table view.
  • UITableViewController will automatically make itself the data source and delegate, unless you specifically change it.
  • The UITableViewController will reload the table view’s data the first time it’s loaded. It will also clear any selected rows whenever the table view is displayed.
  • After the table view appears, it will flash the table view’s scroll indicators. This is a hint to the user that there’s more data than they currently see on the screen.
  • If there’s a navigation bar with an Edit/Done button, the UITableViewController will hook it up to toggle the edit mode of the table view.
Basically, it saves a little bit of time by automatically implementing some common and expected code. If you don’t want any of this behavior, you can always do it yourself within a UIViewController. Just remember to manually implement the steps listed above yourself, if you still want them. You might have seen apps where you select a row from a table, go to a new screen, and when you come back the row is still highlighted. This is usually a sign that someone used a UIViewController with their table view and forgot to clear the selection :) .
The most common times when I don’t use a UITableViewController in my apps is usually when the view controller needs extra functionality beyond just a table view. Perhaps I want the table view nested inside another view, for example. Usually though, you can use a UITableViewController, as we do in the example below.
A helpful hint: if you’re creating a new view controller though Xcode (like under File -> New -> New File…), you can select “UIViewController subclass”, and then on the next screen, choose “UITableViewController” from the “Subclass of” drop down.
UITableViewDataSource
Say we have an app that displays a list of countries in a table view. (A reminder that I’ve made an example project that does this available here: UITableView-Tutorial).
We have a UITableViewController subclass called CountriesTableViewController. Countries are stored in an NSDictionary called “countries”. The keys in the dictionary are the continents in the world, and each value is an NSArray with the countries in that continent.
The first method we’ll define is “- numberOfSectionsInTableView:”. This is the number of keys in the countries dictionary:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [self.countries count];
}
Now we add a way to get the title of a given section:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [[self.countries allKeys] objectAtIndex:section];
}
Next, we provide a way to return the number of rows for a given section. We can use the method we just defined to get the name of the continent:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSString *continent = [self tableView:tableView titleForHeaderInSection:section];
return [[self.countries valueForKey:continent] count];
}
The one required method left is to return the cell for a particular row.
cellForRowAtIndexPath and UITableViewCells
The method “- tableView:cellForRowAtIndexPath:” is an important one. We’re given an index path. An index path is an object that contains both a section and a row. Given this information we can determine which row we’re looking for and configure a UITableViewCell to match it.
The class UITableViewCell represents a single cell (or row) in the table view. If a table has thousands of rows (say, in a list of contacts), then for iOS to have to create a thousand UITableViewCell objects would be a large resource hog. Also, when a user swipes down a table view list, iOS has to setup these rows very quickly, and creating separate UITableViewCell objects as you scroll takes a lot of time.
This is why UITableViewCell uses the idea of a “cell identifier”. When you create a UITableViewCell, you can give it a cell identifier string. After that, calling “dequeueReusableCellWithIdentifier” will attempt to return a pre-existing UITableViewCell, saving iOS from having to create a new one. It can cycle through 10 or so table view cells that are visible at any one time.
You usually only need different cell identifiers for each separate cell layout in your table. In many cases, there is only one, if each cell is laid out the same way. That is also the case for our example.
UITableViewCell is a very versatile class. It has a number of styles pre-defined, including a single line of text, a line of text with a subtitle, and image views. You can also setup the accessory view, which is the symbol you sometimes see on the right side of the cell (often as a “>”). If you need to further customize UITableViewCell, you can get access to its contentView, or even subclass UITableViewCell for complete control.
Let’s setup a simple cell with the country name on the left, and an accessory indicator on the right:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"CountryCell";
 
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
 
// Configure the cell...
NSString *continent = [self tableView:tableView titleForHeaderInSection:indexPath.section];
NSString *country = [[self.countries valueForKey:continent] objectAtIndex:indexPath.row];
 
cell.textLabel.text = country;
 
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
 
return cell;
}
UITableViewDelegate
The code we’ve written above is actually enough to show the list of countries within sections of continents. However, it doesn’t provide a way to respond to any actions, such as the user tapping on a row of the table. That’s where UITableViewDelegate comes in.
Let’s add in some code that presents the country in a UIAlertView when its tapped. For this we define a method called “– tableView:didDeselectRowAtIndexPath:”.
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *continent = [self tableView:tableView titleForHeaderInSection:indexPath.section];
NSString *country = [[self.countries valueForKey:continent] objectAtIndex:indexPath.row];
 
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:nil message:[NSString stringWithFormat:@"You selected %@!", country] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
[alert release];
 
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
We’re given an indexPath, which we can use to find the country that was selected. Then we display that in an alert. Lastly, we deselect the row. Without this last part, the row would stay highlighted after being tapped.
Conclusion
I hope you’ve found this UITableView tutorial helpful. UITableView and its related classes are used in almost every iOS app. There were some more complex things that I didn’t talk about here, including inserting, reordering and deleting rows, and coding up custom UITableViewCells. If you’d like to know more about those or any other topics, let me know in the comments! It might be our next tutorial!

No comments:

Post a Comment