Reusable collapsable table view for iOS
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.
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
Post Comment
Yay google is Yay google is my queen aided me to find this great site!.
Thanks-a-mundo for the blog.Much thanks again. Much obliged.
Well I really enjoyed reading it. This tip offered by you is very helpful for accurate planning.
you got a very excellent website, Glad I observed it through yahoo.
I will immediately snatch your rss feed as I can not in finding your e-mail subscription link or e-newsletter service. Do you have any? Please let me recognise so that I may subscribe. Thanks.
This is one awesome blog.Really thank you! Really Great.
You are my inspiration , I own few web logs and infrequently run out from to brand.
Thanks-a-mundo for the blog.Much thanks again. Will read on
Thank you for sharing your thoughts. I really appreciate your efforts and I am waiting for your further write ups thank you once again.
You could certainly see your skills in the work you write. The world hopes for even more passionate writers like you who aren at afraid to say how they believe. Always follow your heart.
Major thanks for the blog article.Much thanks again. Fantastic.
wow, superb blog post.Really pumped up about read more. Really want more.
Very nice post. I just stumbled upon your blog and wished to say that I have truly enjoyed browsing your blog posts. In any case I will be subscribing to your rss feed and I hope you write again soon!
Thank you for your post.Really thank you! Really Great.
Really appreciate you sharing this blog article.Really thank you! Want more.
Thanks for sharing, this is a fantastic article post.Much thanks again. Really Great.
This very blog is no doubt entertaining and besides diverting. I have picked helluva helpful tips out of this source. I ad love to go back every once in a while. Thanks a bunch!
Major thankies for the article post.Really thank you! Fantastic.
Usually I don at read article on blogs, but I wish to say that this write-up very forced me to take a look at and do so! Your writing taste has been amazed me. Thanks, very nice article.
It as truly a great and helpful piece of information. I am glad that you shared this helpful tidbit with us. Please stay us up to date like this. Thanks for sharing.
Thanks for the blog.Thanks Again. Much obliged.
I will immediately snatch your rss feed as I can at in finding your e-mail subscription link or e-newsletter service. Do you ave any? Please allow me know so that I may just subscribe. Thanks.
Very informative blog article.Really looking forward to read more. Will read on
Well I really liked studying it. This subject provided by you is very practical for accurate planning.
on your blog. Is this a paid theme or did you modify
Say, you got a nice article.Really thank you! Fantastic.
Thanks so much for the blog.Much thanks again. Really Cool.
I used to be able to find good advice from your articles.
Just wanna tell that this is extremely helpful, Thanks for taking your time to write this.
Thanks so much for the blog article.Thanks Again.
I truly appreciate this article post. Cool.
Whats up are using WordPress for your blog platform?
you ave got an incredible weblog right here! would you like to make some invite posts on my weblog?
Perfectly pent subject matter, Really enjoyed looking through.
Very informative article post. Really Cool.
It'аs really a nice and helpful piece of info. I'аm glad that you just shared this helpful information with us. Please keep us up to date like this. Thank you for sharing.
I really liked your article.Really looking forward to read more. Keep writing.
Looking forward to reading more. Great blog. Cool.
pretty useful stuff, overall I consider this is worthy of a bookmark, thanks
Thanks-a-mundo for the article.Thanks Again. Cool.
Utterly written subject material, appreciate it for selective information.
Very informative blog article.Really looking forward to read more. Cool.
I really liked your article. Really Cool.
Major thanks for the article. Really Great.
My brother recommended I might like this website. He was totally right. This post truly made my day. You cann at imagine simply how much time I had spent for this information! Thanks!
Thanks for the post. I will definitely comeback.
This blog was how do you say it? Relevant!! Finally I ave found something which helped me. Thank you!
Say, you got a nice post.Thanks Again. Cool.
I value the blog post.Really looking forward to read more. Will read on
This is very interesting, You are a very skilled blogger. I have joined your rss feed and look forward to seeking more of your great post. Also, I ave shared your web site in my social networks!