Oct 12, 2008

Organizing a complex UITableView with the iPhone SDK by using a dispatch table

Hi. I've been working on an iPhone SDK application for a while, and one major piece of this application is a fairly complex table view. The UITableView is the workhorse class of most iPhone applications - you see it everywhere. One problem with the table view is that the controller class tends to get very large and complex, especially if you have a multiple section table view that has different types of custom cells and behaviors. There are many things that can be done to make this job easier. One excellent technique was recently documented by Frasier Speirs: A technique for using UITableView and retaining your sanity.

I've developed a technique that can make use of this technique but that goes beyond it by allowing you to delegate the handling of each section in a table view to its own class. It is also easily configurable if you need to add or remove a section type from your controller later, and allows you to localize your changes so that you don't have to remember to modify code in several places.


As I'm sure many of you know, the UITableViewDelegate protocol contains about 16 methods that take either an NSIndexPath object that specifies a row in a specific section, or an NSInteger that specifies a section.

The most obvious way to implement this would be through a switch statement in each of these methods. In the case of my program, this rapidly became unworkable, since I have 5 different sections, some with their own custom view types and specific logic for each section that uses a different portion of the data model and logic to generate the table view.


The basis of my technique is to create a main view controller for the table view, in this case called NoteViewController, and separate section controllers for each section that inherit from an AbstractNoteSectionController class that I created. This class defines the interface for the section controllers and provides reasonable default behavior in case I don't need specific behavior in my subclasses. The interface for this class is:

@interface AbstractNoteSectionController: NSObject {
NSUInteger section;
NoteViewController *parent;
}

@property NSUInteger section;
@property (nonatomic,retain) NoteViewController *parent;

- (id)initWithNoteViewController:(NoteViewController *)cnt
                      andSection:(NSUInteger)aSection;

- (NSUInteger)numberOfRows;

- (UITableViewCell *)cellForRow:(NSUInteger)row;

- (UITableViewCellEditingStyle)editingStyleForRow:
   (NSUInteger)row;

- (void)selectedRow:(NSUInteger)row
    withEditingStyle:(UITableViewCellEditingStyle)style;

- (void)commitEditingStyle:(UITableViewCellEditingStyle)style
                    forRow:(NSUInteger)row;

- (BOOL)canEditRow:(NSUInteger)row;

- (BOOL)shouldIndentWhileEditingRow:(NSUInteger)row;

@end

I've only implemented the table view delegate methods that I need for my application. I then subclass this and implement the specific functionality for each section in a separate subclass. In the main NoteViewController class, I have a sectionDict instance variable that has an NSDictionary object keyed by the section number:

// Build a dispatch table so we can more
// easily navigate all the sections.
- (NSMutableDictionary *)buildSectionDict {
  return [NSMutableDictionary dictionaryWithObjectsAndKeys:
    [[[FirstSectionController alloc]
      initWithNoteViewController:self
                      andSection:FIRST_SECTION] autorelease], 
    [NSNumber numberWithUnsignedInteger:FIRST_SECTION],
...
    [[[LastSectionController alloc]
      initWithNoteViewController:self
                      andSection:LAST_SECTION] autorelease],
    [NSNumber numberWithUnsignedInteger:LAST_SECTION], nil];
}

Then, I have a method to select the section controller given a section:
- (AbstractNoteSectionController *)
    sectionControllerForSection:(NSUInteger)aSection {
  NSNumber *section =
    [NSNumber numberWithUnsignedInteger:aSection];
  return (AbstractNoteSectionController *)
    [sectionDict objectForKey:section];
}
After this, something like tableView:numberOfRowsInSection: is reduced to this simple piece of code from a fairly complex and difficult to maintain switch statement:

- (NSInteger)tableView:(UITableView *)tableView
  numberOfRowsInSection:(NSInteger)section {
  return [[self sectionControllerForSection:section]
    numberOfRows];
}

This allows each section controller to be focused and readable. For example, the numberOfRows method for the PhotosSectionController is very simple.

- (NSUInteger)numberOfRows {
  return ((self.cameraAvailable) ? 2 : 1) +
      [parent.note countOfPhotos];
}

I suppose that I should mention at this point that each section controller has a parent member that points to the NoteViewController which still mediates access to the model (parent.note)


I love programming with this type of dispatch table. It makes for extremely flexible code that is not at all brittle, because I can add or remove sections from the table view by simply editing the buildSectionDict method and writing a new subclass of AbstractNoteSectionController.  No other methods of the NoteViewController ever have to change, which is great because it gives me fewer things to think about.

Using this technique, I was able to split up a hard to read and manage 700 line behemoth class into much more managable 80-200 line pieces and improve the readability and flexibilty of my code immensely. Hope you find this useful in your iPhone development.

10 comments:

Unknown said...

First, great article. I am using this as the basis to create a mutable array of table sections. This will allow me to remove particular sections based on state (for example a basic vs. advanced version) and have the table redraw with only the visible sections.

Also, I can use the order of the array to dictate the section order rather than have it as part of the abstract class.

I am having an issue when including the parent though...

If I just send in the UITableView when initializing the section class it works fine, but once I try to include the controller (as you have for your parent) I get errors that say the following:

error: cannot find interface declaration for 'Abstract...', super class of '...TableSectionController'

There is an error for every sub class of the abstract class I have and it shows up right below the .h reference in the .m for each subclass... :-S

Would it be possible for you to post your code so I can see if I am just making a stupid mistake?

I am trying to narrow down why this is happening and it may be circular references or something else, but it is not easy to tell at this time...

Thanks in advance.

Paul Franceus said...

Thanks, Sounds like you are making some good extensions.

First of all, in the parent class NoteViewController, I put an @class declaration:

#import <UIKit/UIKit.h>
#import <AddressBook/AddressBook.h>
#import "NoteList.h"
#import "Note.h"
#import "PixieNetNodeDescriptor.h"

@class AbstractNoteSectionController;

@interface NoteViewController : UIViewController <UITableViewDelegate, UITableViewDataSource, UIActionSheetDelegate> {


That prevents the circular reference. If that doesn't solve the problem for you let me know and I'll try to post the code somewhere.

Unknown said...

I added the class reference to the table view controller and the abstract class, but it still gives me errors.

If you wouldn't mind, it would probably be helpful to see the code (or at least the headers) of the table view controller, the abstract class and the classes that inherit from it.

Thanks for all the help.

Paul Franceus said...

I'd prefer not to share this with the world. Please email me and I'll send you a link. You should be able to get email from my profile

uri said...

Simply perfect. I used your implementation and now my 6 section Table is extremely easy to use and extend! Thanks a lot for this great post.

Andy said...

It's a nice approach - I'm just wondering why you're using an NSDictionary thats keyed with an integer - why not just use an array ?

If you dynamically removed, say, the table section keyed by '2' then you would have to iterate through the whole dict and readjust the indexes. If you don't do this, you'll get a crash when the delegate is asked to supply the rows for the missing section in the table.

If you just used an array, removing an item would automatically shuffle the rest of the elements to fill the gap.

Paul Franceus said...

Why not an array? No reason especially, I guess I usually use maps or dictionaries for doing dispatch tables like this, but you are right, in this case an array would work.

vJ said...

Great Article!!!
I like the way the moduled approach in handling different sections. I am wondering about the default behaviour of the default class and how to implement these. For example should I return a Zero sized UITableCell
and also(off Topic) I have a question about reusing the cell. I see evrywhere dequeueReusableCellWithIdentifier but each cell is Unique and how will it get reused.

Paul Franceus said...

vJ

What to put in the abstract class is up to you - whatever makes sense for your implementation.

I suggest you read about UITableViews and cells. Generally, you reuse a cell, and just change the content as you go. Typically, the cells in a table view have generally the same layout - perhaps a line of text or two and an image, or some other layout. The only difference is the content. I wouldn't expect that the rows of a table are completely unique. If you happen to have that then you can't use the reusable cell, but if it's just the contents that change but the layout is the same, then you can.

vJ said...

Thanks Paul.
As I come from a Non Apple world. Posts like yours give me lots of information and motivation.
I have just started writing an recipe kind of application where I did not use UITableView. I just used viewcontrollers for each section and each viewcontroller has methods which will create labels(with/without images,checkmarks) as needed and also has property for approximate view size and I add the views from these controllers into the main view using the approximate sizes for size and location of each section. This I know this may be bad but went ahead and wrote as it was for my learning purpose(code snippets below).

Then I found your intresting article and from links from your article about multiple lined lables which will make writing this app much more easier. I still have a few questions...

If there is no default behaviour which I could have had in the main ViewContoller (NoteViewController)
can I just make this a protocol?

Also when i wrtie my UITableViewCells for a particular section the content of a cell may be varying a lot like the color from prev cell, height based on text and therefore I dont know from documentation internally how the cells layout can be completely reused but I always see in all code its always dequed and reused by a same named identifier.

Sorry for the long post and bad english :-) Also would be great if I can get a skeleton of the main overview classes of your project and if it is okay I will send a request to your mail ID.


-- Setting a recipe and also finding approximately
approximate size of ingredientsview.
-(void) setRecipe:(Recipe* ) newRecipe{

CGSize largeBigSize = CGSizeMake(kMaxWidth-kColumnOffset, kMaxHeight);
CGFloat cHeight;
CGSize ingredientSize;
int ingredientPosition =1;


if(newRecipe!=recipe){
[newRecipe retain];
[recipe release];
recipe = newRecipe;
}


ingredientSize = [kIngredientHeader sizeWithFont:[UIFont systemFontOfSize:kMainFontSize]
constrainedToSize:largeBigSize
lineBreakMode:UILineBreakModeTailTruncation];
cHeight +=ingredientSize.height+kRowOffset;

NSMutableArray *ingredients = [recipe ingredients];
for (Ingredient *ingredient in ingredients) {

//NSString *ingredientDescription = [ingredient ingredientDescription];
NSString *ingredientDescription = [NSString stringWithFormat: @"%d. %@",ingredientPosition,[ingredient ingredientDescription]];

ingredientSize = [ingredientDescription sizeWithFont:[UIFont systemFontOfSize:kSecondaryFontSize]
constrainedToSize:largeBigSize
lineBreakMode:UILineBreakModeTailTruncation];
cHeight +=ingredientSize.height+kRowOffset;
ingredientPosition++;
NSLog( @"Ingredient %@ width: %f height: %f ",[ingredient ingredientDescription],kMaxWidth,cHeight);

}

approximateSize = CGSizeMake(kMaxWidth,cHeight);


}



And in viewDidLoad I do this.




RecipeInstructionsView *instructionsView = [[RecipeInstructionsView alloc]init];
[instructionsView setRecipe:recipe];
[instructionsView setBackgroundColor:[UIColor blackColor]];
CGSize instructionsViewSize = [instructionsView approximateSize];

[instructionsView setFrame:CGRectMake(0.0, kRecipeCellViewHeight+ingredientsViewSize.height, ingredientsViewSize.width, kRecipeCellViewHeight+ingredientsViewSize.height+instructionsViewSize.height)];
[self.view addSubview:instructionsView];