"When one teaches, two learn..."
A blog about iPhone development.
Friday, April 24, 2009
iPhone tutorial: "Navigation-Based Application" part 2
In the last part we mainly analysed the IB files of the "Navigation-Based Application" XCode template. This time we're going to look at the source and also do some modifications to it. This part mainly focuses on the table view (UITableView) so the title of this post could have been "UITableView tutorial".
If you scroll through "Classes/RootViewController.m" in XCode, you'll notice that it mainly consists of methods that are commented out. The reason for why there are so many methods is that this class has a lot of "responsibility". If you double-click on Resources/RootViewController.xib in XCode to load the file into Interface Builder (IB) and then CTRL-click on "File's Owner" in the RootViewController.xib window in IB, you'll see that it has two referencing outlets; 'Table View.dataSource' and 'Table View.delegate', which means that our RootViewController object has been connected to these properties of the "Table View" object in this IB file. The 'tableView' property of our RootViewController has in turn been connected to the "Table View" object. This probably means that we'll receive messages from "Table View"...
Since "Table View" is of the class UITableView, we should look up that class in XCode's API docs window. Under the "Properties" section in the docs for UITableView we see that the object assigned to the 'dataSource' property should implement the UITableViewDataSource protocol and the object assigned to the 'delegate' property should implement the UITableViewDelegate protocol.
That's what we meat by a lot of responsibility above - it is expected to implement these two protocols. The 'dataSource' is intended to supply the actual contents of the table, while the 'delegate' focuses on the presentation of the contents.
According to the API docs in XCode this protocol "provides the the table-view object with the information it needs to construct and modify a table view.", but also that it "supplies minimal information about it's appearance".
As usual in protocols, a lot of the methods are optional. The only required methods in this protocol are 'cellForRowAtIndexPath' and 'numberOfRowsInSection'. UITableView's definitoin of a table is that it has a number of rows, where each row has a cell which contains the actual contents. Furthermore, a table can be divided into sections, where each section contain a number of rows. This way rows that belong together can be "grouped" and each group (or section) can have its own title, etc.
That should explain the "row", "cell" and "section" parts of the method names, but what on earth is an "index path"?
One way to visualise an index path is to think of a document which is divided into chapters, sections, paragraphs, sentences and words. If you want to "point at" a specific word in the document you could specify that it is in chapter 3, section 2, paragraph 1, sentence 7 and word 5. The "path" to that word would be 3, 2, 1, 7, 5 or perhaps /3/2/1/7/5 to make it look more like the paths used to navigate the file system on a computer or page structure on a web site.
If you wanted to present the contents of a book hierarchically using tables you could start by presenting a table listing all the chapters in the book. When the user selects one of the chapters from the table, you switch to a table listing all the sections in that chapter. When the user selects a section, you switch to a table of all the paragraphs in that section, and so on...
In this setup you would have a chapter table, section table, paragraph table, sentence table and word table. All tables of a certain kind (chapter, section, etc.) would look alike (visually), but would have different content, since chapter 2 probably doesn't contain the same sections as chapter 1, etc.
Thus, index paths can be used to "point at" or reference a specific item in a hierarchy of items or a specific node in a tree if you're familiar with that term. In our context it is used as a reference to a specific row in a hierarchy of tables. If you want to know even more about "index paths" I suggest you do a seach for NSIndexPath in XCode's API docs window since that is the class which implements index paths.
As we said before, a table consists of a number of rows which can be divided into sections ("grouped together"). Even though that it sounds like a "section" is a new level in the hierarchy of tables, that is not the case. Instead, it is a "level" inside the table. Say that we have a table with 5 rows, divided into two sections; the first section has 2 rows and the second has 3:
row 0: section 0, row 0
row 1: section 0, row 1
row 2: section 1, row 0
row 3: section 1, row 1
row 4: section 1, row 2
We see that there are two ways to "point to" a specific row in this table; either you specify only the row (0-4) or you speficy the section and row within the section. We also see that it is possible to translate between these two types of "pointers"; row 3 is the same as "section 1, row 1", for example.
A more compact way of describing the "structure" of the table above would be
section 0: 2 rows
section 1: 3 rows
and that is exactly the method which UITableView uses and the information that the 'numberOfRowsInSection' method should supply. The method gets called with the section (0, 1, ...) as an argument and is expected to return the number of rows for that section.
By the way, did you notice anything strange with the title of this section? See that the method name actually starts with "tableView:"? In Objective C, you could say that the "name" of this method actually is "tableView" and that it has an argument called "numberOfRowsInSection". Or you could say it's a method with no name that has two arguments; 'tableView' and 'numberOfRowsInSection'. Ok, let's stop playing around - the name of this method is "tableView:numberOfRowsInSection" and it has two arguments - nothing strange about that! ;)
So, what is the 'tableView' argument good for? That is used to identify which tableView that wants to find out how many rows it has. Specifying the tableView makes it possible for a single object to be the data source for multiple (different) tables.
If you want to create a table with more than one section you have to implement the optional method 'numberOfSectionsInTableView' as well. Otherwise it is assumed that the table only has one section (section 0).
Note that this method, just like 'numberOfRowsInSection', specifies the tableView requesting information, but here it is the last argument instead of the first. Don't ask me why.
Now that the structure of the table is defined it's time to fill it with some contents. That's when we enter the world of the cells, since it is those that contain the actual contents. A specific cell in a hierarchy of tables is referenced using an "index path", just as we referenced specific words in a book in the explanation of index paths above. An UITableView will ask you to supply a cell for it by giving you an index path - a brutal but quite simple and effiecient way of solving a rather complex problem.
This method should return a pointer to an UITableViewCell object, so let's start by looking at the API docs for that class in XCode. Whoa, that's a huge document! I think we'll limit ourselves to the basics in this part of the tutorial.
It can't get more basic than creating and initialising an object, and initialise is exactly what this method does. If you wonder how to create the object you, as always, call the alloc-method on the class; [UITableViewCell alloc].
If you check the XCode API docs for this method, you'll quickly find out that it is possible to call this method with CGRectZero (a "zero sized" rectangle) as the 'Frame' argument and nil as the 'reuseIdentifier' argument. So what are those arguments for if it you more or less can skip them? The API docs go on to say that the 'Frame' needs to be properly specified if you need a complex layout for the cell's contents and 'reuseIdentifier' is used if you want to reuse a cell. Being able to reuse a cell is a performance optimisation and as often is the case, added performance leads to added complexity. This time in the form of a "strange" argument at initialisation. We'll return to this later.
Mandatory method from the UITableViewDataSource protocol. This method was thoroughly explained above. Returning 0 sounds a bit boring though, so I suggest you change this to 3 and build and run (CMD-Return). Wow, what a difference! No? You don't see any difference at all? Hmm, strange... But hey, let's try clicking on any of the first three rows in the table in the iPhone simulator. See, they light up in blue. Now try pressing any of the other rows. Nothing happens right? So the value 3 that we returned seems to have accomplished something, right?
I thought I wouldn't have to explain how to reuse table cells, but such a strange method name as 'dequeueReusableCellWithIdentifier' makes it hard to avoid, so here we go...
This method is part of the UITableView and the reason for that is that it is that class that actually "manage" the cells. It justs asks the 'data source' to create the cell for it, but once it is created it is managed by the table view. Remember that we mentioned 'reuseIdentifier' and something about performance optimisations when we discussed how to create and initialise a table cell? Well, this is another part of the optimisation scheme.
The scheme basically assumes that most (or many) cells in a table will look alike but contain different contents. For example, all cells (rows) might have a white background, but the text for each cell (row) might be different. So when the table view asks the data source for a cell, the data source can assign an "identifier" (name or "description") for the cell. In our example this identifier could have been the string "cell with white background" or maybe just "white" or why not "cell 1" or just "1". Ok, I guess you get it, it can be named anything really.
When the table view gets this cell from the data source it caches it together with its identifier. Later when the data source needs to supply another "white" cell to the table view it can ask the table view if it still has it in its cache and that is exactly what this method does.
If the table view didn't know anything about the cell the data source wants to supply to it, the dequeue-method returns nil and that's why we check for nil here. This also means that we have to create the cell and that's why we call 'initWithFrame' and supply a 'reuseIdentifier'.
// Set up the cell...
The comment seems to suggest that we should do something with the cell after creating and initialise it. Perhaps because it's a bit boring and not very useful having a table with just "blank" cells in it? Quickly checking the API docs for UITableViewCell in XCode we see that there is a property called 'text', so let's set it by inserting the following right above the return-line:
cell.text = @"My cell";
Build and run (CMD-Return) and - finally! - you should see somthing else than a boring white background with some thin grey lines on it! More specifically, you should see three rows with the text "My Cell".
Optional method from the UITableViewDelegate protocol, which, according to the API docs "Tells the delegate that the specified row is now selected". As you can see it references the row by supplying an index path.
// Navigation logic may go here. Create and push another view controller.
Lots of comments that suggest that we should do something here, which is a good idea because otherwise we would just have a "dead" table with almost no interaction. Sure, you would still be able see the contents of the table and actually scroll through it, so a "dead" table isn't completely dead, just a little boring.
To make it a little less boring, we'll log something to the console, so insert the following just above the final curly bracket:
NSLog(@"row %d was selected", indexPath.row);
Build and run (CMD-Return) and wait for the table to appear in the iPhone simulator. Once it has, select the source window in XCode and press (CMD-R) to bring the console window to the front. Go back to the simulator and press any of the "My Cell" rows and you should see something like this in the console window:
2009-04-26 08:17:19.693 Nav1[9992:20b] row 0 was selected
2009-04-26 08:17:20.868 Nav1[9992:20b] row 1 was selected
2009-04-26 08:17:21.676 Nav1[9992:20b] row 2 was selected
In part one I told you that the table view was a real beast and perhaps you agree by now. Sure, it isn't completely wild and unhandleable, but it sure needs to be tamed before it is of any use to you. We've come some way towards achieving that and now we actually understands what happens in this XCode template project, but I wouldn't be surprised if more will be written on the topic of table views in this blog...