Saturday, May 23, 2009

iPhone tutorial: UITableView from the ground up, part 1

In a previous post (which I recommend that you read), I wrote quite a lot about UITableView, but I also mentioned that I would probably return to the subject in the future. Now that time has come! The reason for this is that I'm still a bit scared of the class and always try to avoid it when designing user interfaces. Sure, there are merits in fitting all information on a single screen since it forces you to really think about how to interact with your application and how to minimise the number of settings. However, understanding a powerful class like UITableView also has its merits since it can save you a lot of time during both the design and implementation phases. Furthermore, it gives the user a consistent experience over all iPhone applications instead of having to learn each application's interface.

Avoiding the UITableView isn't that difficult as long as you're working with small pieces of information, but what do you do if you have a list of a hundred things you want to display? Choose a really small font? Use a tab controller with five tabs showing 20 things per tab? Use a navigation controller with "Next"-buttons? Use a UIScrollView? As you can see, there is no shortage of alternative solutions and some can probably work very well, but you should probably have a very strong reason for inventing something new opposed to using the UITableView since this is designed to show long lists of information.

As you can see in the title of this post, this is part 1 of a series of UITableView tutorials. This is because the subject is huge and I think it will be easier to understand piece by piece instead of all at once even if it means that some overview is lost. To compensate for the lost overview I will try to post the parts in quite tight succession.

Let's create the XCode project for this tutorial! Start XCode and choose "File/New project" from the menu. Select the "Window-Based Application" template and name it "Table1".

Resources/MainWindow.xib

Double-click on MainWindow.xib to start Interface Builder (IB) and load the xib-file. As always when doing anything but the most trivial tasks in IB, I recommend that you switch to hierarchical view mode by pressing the middle icon above the "View mode" text in the upper left corner. Bring up the library window in IB (CMD-L) and drag the "Table View" icon from the "Data views" section and drop it onto the "Window" object in the MainWindow.xib window. A small arrow should appear to the left of the "Window" object to indicate that other objects are embedded into it. Press the arrow and the hierarchy under the object should appear; in this case just the "Table View" object. If you double-click the "Table view" object the "design window" for that object appears, in this case the design window shows a UITableView with a list of cities in California.

Now we should make the "Table1 App Delegate" object the delegate of the "Table View" object, so CTRL-drag from "Table View" to "Table1 App Delegate", first to connect it to the 'dataSource' outlet and then to the 'delegate' outlet.

Classes/Table1AppDelegate.h

Above, we connected the application delegate object to both the 'delegate' and the 'data source'for our table, so let's specify that this class is going to implement the UITableViewDelegate and UITableViewDataSource protocols. We do this to allow the compiler to help us check that we have implemented all the required methods. Do this by editing the h-file so that the interface-line looks like this:

@interface Table1AppDelegate : NSObject <UIApplicationDelegate, UITableViewDelegate, UITableViewDataSource> {

If you build the project (CMD-B), you'll see that the compiler issues the following warnings:

Table1AppDelegate.m:29: warning: incomplete implementation of class 'Table1AppDelegate'
Table1AppDelegate.m:29: warning: method definition for '-tableView:cellForRowAtIndexPath:' not found
Table1AppDelegate.m:29: warning: method definition for '-tableView:numberOfRowsInSection:' not found
Table1AppDelegate.m:29: warning: class 'Table1AppDelegate' does not fully implement the 'UITableViewDataSource' protocol

From this output we can immediately see that we need to implment the 'tableView:cellForRowAtIndexPath:' and 'tableView:numberOfRowsInSection' methods of the UITableViewDataSource protocol. I wrote quite a lot about these methods in my first UITableView post so I won't do a full "background" here, but instead focus on the implementation.

If you want to find out what happens if you ignore this warning and refrain from implementing them, go ahead and build and run (CMD-Return) the application and watch it crash in the simulator. After the crash, check out the XCode console window (CMD-R) to see what went wrong. You'll see something like this:

[Session started at 2009-05-23 09:58:36 +0200.]
2009-05-23 09:58:40.887 Table1-2[7110:20b] *** -[Table1AppDelegate tableView:numberOfRowsInSection:]: unrecognized selector sent to instance 0x523870
2009-05-23 09:58:40.893 Table1-2[7110:20b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[Table1AppDelegate tableView:numberOfRowsInSection:]: unrecognized selector sent to instance 0x523870'

It seems someone tried to call 'numberOfRowsInSection', which wasn't implemented ("unrecognized selector sent to instance...").

Classes/Table1AppDelegate.m

numberOfRowsInSection

We'll start with 'numberOfRowsInSection' since that's the most basic of the two. It lays the foundation for the table by specifying how many rows it should have, which in our first example will be three (3). Add the following to the end of the file.

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 3;
}

When I first saw this method I really thought that there was some magic going on - I couldn't grasp what the stuff before the 'numberOfRowsInSection' part of the definition meant. My initial thoughts was that perhaps this method returns two things; a UITableView pointer as well as an NSInteger? Or is there some method overloading or other inheritance-realted Objective C-magic going on?

A short digression on Objective C method names

Once the chock of the strange syntax had worn off , I realised that this method just have a rather strange name. Or actually, you could say it has no name at all - all it has is a list of argument names! If we break it down, it looks like this:

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

So there are three components; the first is the return value, the second is argument 1 and the third is argument 2. Each argument is composed of a "label" or short descrptive text ("tableView" and "numberOfRowsInSection"), a colon (':') and the argument name ("tableView" and "section"). Well, that is not the full truth. It could also be broken down like this:

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

Here we have four components; return value, method name, argument 1 and argument 2. Here the "label" for argument 1 ("tableView") is instead used to specify the method name.

Confusing, huh? In fact, this is all pretty standard Objective C - the method name and first argument label/descriptive text are fused into one - but what's "wrong" with this method in my view is the choice of the method name and/or argument label names. However, there actually exists an acceptable explanation for this naming scheme. Lot's of methods in "delegation" protocols are named according to this scheme and the reason for that is to provide the "delegation context" as the first argument. This is because a single object could be the delegate for several tables for example. If so, the delegate object needs to find out which table is requesting information from it. That's why the first argument is a UITableView pointer.

A more intutive name for this method could have been achieved by rearranging the components a bit:

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

cellForRowAtIndexPath

After the digression on Objective C method names above you probably understand why I left out the tableView "prefix" of the method name in the title above - it's so much easier to refer to the method by just saying 'cellForRowAtIndexPath' even though it's correct name is 'tableView:cellForRowAtIndexPath' since it is defined like this:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

This method is called by the table to create the objects used to display the contents of a cell (row) in the table, that is, objects of the class UITableViewCell. Let's do just that - create an UITableViewCell object and return it by adding the following to the end of the file.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:nil];
cell.text = @"my cell";
return cell;
}

Two things are worth explaining here, or rather worth mentioning since the explanation is available in the API docs for 'initWithFrame:reuseIdentifier' in the UITableViewCell class. What I'm talking about is 'CGRectZero' which is used to specify the size of the cell and the 'nil' value for the 'reuseIdentifier'. We pass 'CGRectZero' since the API docs tells us to and 'nil' since we don't want to reuse the cell. By the way, we'll talk about reusing cells in the next part of this tutorial.

Build and run (CMD-Return) in XCode and you should see a table containing three rows, all containing the text "my cell". Try touching the rows and they should turn blue when they are selected. If you try to select the fourth row, nothing will happens since our table only has three "active" rows. Maybe not that impressive, but also not that complicated to make, huh?

Summary

We have managed to display a table view with just a few lines of code and have hopefully managed to overcome some of our fear of table views. As always in my tutorials, I do "cheat" a little to keep things simple. For example, I do know that proper memory management is very important, but at this point it would just be in the way and introduce unwanted complexity. Furthermore, it's often quite easy to add once you understand the underlying classes you're working with.