Introduction

I was once working on an iPhone application that shows a large number of inputs, grouped into various categories, in a UITableView. To change the value of one of the inputs, the user presses the corresponding row in the table view and alters the value in a separate screen that appears. The table view had a section for each category and each section contained a table cell (row) for each input.  

The problem was that the number of inputs became very, very large so that it didn't give the user a very good overview. It was even tedious to scroll from the top of the table to the bottom.

We decided that the user should be able to collapse and expand the sections (categories) of the table by simply pressing the header of the section. We required that the code that achieves this should be reusable and that it requires the least possible number of changes to the existing code. 

The screenshot below shows what the table view, with its collapsable sections, looks like.

collapsable_table_view.png

Implementation  

I figured that the best way to achieve the objectives mentioned above was to create a sub-class of the UITableView class, named CollapsableTableView. This ensures that the code is reusable. If done right, no changes would have to be made to the delegate or data source of the UITableView - they would handle the table view like a regular UITableView. The only necessary change would be to change the class of the UITableView in the xib file to this new sub-class. In order to ensure that the client can use the table view like a regular UITableView, we must try to allow the table view to be manipulated entirely through the interface of the UITableView class, including the UITableViewDelegate, and the UITableViewDataSource protocols. 

The collapsable table view must somehow keep track of which sections are collapsed (contracted) and which of them are expanded. Perhaps the most apparent way to do this would be to maintain a set of indices of sections that are expanded, or a boolean array where the value of each index indicates if the corresponding section is expanded or not. However, if we assume that the client of the table view can add and remove sections (which was the case in our scenario), the indices of sections will not remain constant, so working with the indices would be troublesome at best. We must therefore find a different identifier for sections. We could use the header text of sections for this purpose. Of course, this assumes that the header text of a section uniquely identifies that section and that its header text remains constant, but given the constraint of having to stick to the interface of the UITableView class, this is probably the best we can do. This also assumes that the client implements the tableView:titleForHeaderInSection: selector of the UITableViewDelegate protocol for all of the table cells. For our project, this was the case.  In the Using the code-section, we explain how our class also supports clients that implement the tableView:viewForHeaderInSection: selector. 

For easier management of the header views, we create a UIViewController class, named CollapsableTableViewHeaderViewController. For this class there are two xib's. The one xib is used for a table with a plain layout, and the other one is used for a table with a grouped layout. This class contains IB outlets for all the labels in the view that can be manipulated. It stores the index of its section (see CollapsableTableView.m to discover why it is needed), and of course a boolean value indicating if the section is collapsed or not. This view controller class also ensures that its view notifies us when the user taps it, so that the CollapsableTableView can take the necessary action.  

Here is the contents of the .h file of CollapsableTableViewHeaderViewController:

#import <UIKit/UIkit.h>
#import "TapDelegate.h"


@interface CollapsableTableViewHeaderViewController : UIViewController 
{
    IBOutlet UILabel *collapsedIndicatorLabel,*titleLabel,*detailLabel;

    UITapGestureRecognizer* tapRecognizer;

    BOOL viewWasSet;
    id<TapDelegate> tapDelegate;

    int sectionIndex;
    BOOL isCollapsed;
}

@property (nonatomic, readonly) UILabel* titleLabel;
@property (nonatomic, retain) NSString* titleText;
@property (nonatomic, readonly) UILabel* detailLabel;
@property (nonatomic, retain) NSString* detailText;
@property (nonatomic, assign) id<TapDelegate> tapDelegate;
@property (nonatomic, assign) int sectionIndex;
@property (nonatomic, assign) BOOL isCollapsed;

@end

 The collapsedIndicatorLabel is a small label displaying a '-' or a '+' depending on whether the section is collapsed or not. When the value of isCollapsed is changed, the text of the collapsedIndicatorLabel is set to "-" or "+" accordingly. The titleLabel is the label containing the text of the header and the detailLabel shows optional detail text to the right of the title.

Here follows the definition of the TapDelegate protocol:

#import <UIKit/UIKit.h>


@protocol TapDelegate

- (void) viewTapped:(UIView*) theView ofViewController:(UIViewController*) theViewController;

@end

 The viewTapped:ofViewController: selector is called by the CollapsableTableViewHeaderViewController when its view is tapped and CollapsableTableView implements the TapDelegate protocol so that it can collapse or expand the appropriate header.  

In order to detect when the view is tapped we override the setView: selector of the UIViewController class in CollapsableTableViewHeaderViewController like this:

- (void) setView:(UIView*) newView
{
    if (viewWasSet)
    {
        [self.view removeGestureRecognizer:tapRecognizer];
        [tapRecognizer release];
    }
    [super setView:newView];
    tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self 
        action:@selector(headerTapped)];
    [self.view addGestureRecognizer:tapRecognizer];
    viewWasSet = YES;
}

- (void) headerTapped
{
    [tapDelegate viewTapped:self.view ofViewController:self];
}

 Let's return to CollapsableTableView now. When it gets a header title of a section from the client, it needs to be able to retrieve the corresponding CollapsableTableViewHeaderViewController  object (or create a new one if one doesn't yet exist for the specified title). For this we maintain an NSMutableDictionary object that maps a header title to the corresponding CollapsableTableViewHeaderViewController  object. It also comes in handy to have a dictionary that we can use to look up the header title of the section at a specified index (of course this dictionary will have to be updated whenever the client adds or removes a section from the table). 

So, how will the CollapsableTableView actually collapse and expand sections? Well, a collapsed section will simply have 0 rows, so even though the client will return the normal number of rows for the section, the CollapsableTableView will return 0 for the number of rows of a collapsed section, or the number returned by the client for an expanded section. This suggests that CollapsableTableView needs to intercept the calls to the tableView:numberOfRowsInSection: selector. It must also return the view of the corresponding CollapsableTableViewHeaderViewController  for each section, so it must also intercept calls to the tableView:viewForHeaderInSection: selector. So in order for CollapsableTableView to be able to respond to both these selectors, it must implement the UITableViewDelegate and the UITableViewDataSource protocols and at run-time set itself as the delegate and data source of... itself! Many calls to the selectors of these protocols, however, must be forwarded to the client, so CollapsableTableView stores references for the real delegate and data source so that they can be consulted for these cases.

- (void) setDelegate:(id <UITableViewDelegate>) newDelegate
{
    [super setDelegate:self];
    realDelegate = newDelegate;
}

- (void) setDataSource:(id <UITableViewDataSource>) newDataSource
{
    [super setDataSource:self];
    realDataSource = newDataSource;
}

Here is the interface file of CollapsableTableView:   

#import <Foundation/Foundation.h>
#import "TapDelegate.h"


@interface CollapsableTableView : UITableView <UITableViewDelegate,
    UITableViewDataSource,TapDelegate>
{
    id<UITableViewDelegate> realDelegate;
    id<UITableViewDataSource> realDataSource;
    
    NSMutableDictionary *headerTitleToViewControllerMap,*sectionIdxToHeaderTitleMap;
}

- (NSDictionary*) getHeaderTitleToIsCollapsedMap;
- (void) setIsCollapsed:(BOOL) isCollapsed forHeaderWithTitle:(NSString*) headerTitle;

@end

 The implementation of CollapsableTableView pretty much follows from the discussion up to this point. 

Using the code   

The source files that need to be added to an Xcode project in order to use the CollapsableTableView class are those in the CollapsableTableView folder in the zip file (Download CollapsableTableView.zip - 42.79 KB).

CollapsableTableView can be used exactly like a regular UITableView, as long as the client implements the tableView:titleForHeaderInSection: selector (opposed to tableView:viewForHeaderInSection: ) for all the table cells. The only necessary change to be made is to change the class of the UITableView of the xib to CollapsableTableView. To do this, open the xib file, select the UITableView, open the Identity Inspector and type "CollapsableTableView" next to the Class field.

The implementation of CollapsableTableView does also allow the use of tableView:viewForHeaderInSection:, but here it doesn't have access to the title string of the header, which it normally uses as the identifier of the header. Instead, it uses the string "Tag %i", where %i is the value of the tag property of the view that is returned (if tag is 0, but the section index is not 0, this number defaults to the section index in CollapsableTableView).  This means that if the client returns views (instead of header text strings) for some cells, and if it can add and remove sections, it must assign a unique tag number to the view corresponding to each section. 

The client of the CollapsableTableView can be unaware of the fact that it is not working with a regular UITableView, but if it does know that the UITableView is a CollapsableTableView, it can cast the object to the latter type and use the getHeaderTitleToIsCollapsedMap method to determine which sections are collapsed and the setIsCollapsed:forHeaderWithTitle: method to programmatically collapse or expand sections. 

As was mentioned in the implementation-section, CollapsableTableView also allows for detail-text to be displayed to the right of the title of a header. To make use of this feature, in tableView:titleForHeaderInSection:, return a string of the form "Header Text|Detail Text".

History 

2011/08/13 - Initial Version 

推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"