我曾经在iPhone上的应用,显示了大量投入,分为 不同的类别,UITableView的
工作。要改变值的输入,用户按表视图中相应的行之一,并在一个单独的屏幕上出现的改变值。表视图每个类别的一个部分,每个部分包含每个输入表格单元格(行)。
问题是,投入的数量变得非常,非常大,因此,它并没有给用户一个很好的概述。这是从表上方滚动的底部甚至乏味。
我们决定,用户应该能够折叠和展开只需按节头表的部分(类)。我们需要的代码实现这个应该是可重复使用的,它需要修改现有的代码尽可能少的数量。
下面的截图显示的表视图,其可折叠的部分,看起来像。
实施
我想,实现上述目标的最佳途径是创建一个子类的UITableView
类,名为CollapsableTableView
。这确保该代码是可重复使用的。如果使用得当,将有任何改变必须作出委托或数据源的UITableView
-他们将处理像一个普通的表视图的UITableView
。唯一必要的变化会改变之类的UITableView
在厦门国际银行文件到这个新的子类。为了确保客户端可以使用像一个普通的表视图的UITableView
,我们必须尽量让表查看被操纵完全通过接口的UITableView
类,包括UITableViewDelegate
,和UITableViewDataSource
协议。
The collapsible 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 fixed, 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 thetableView: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, namedCollapsableTableViewHeaderViewController
. For this class, there are two xibs. 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 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 theCollapsableTableView
can take the necessary action.
Here is the contents of the .h file of CollapsableTableViewHeaderViewController
:
#import <UIKit/UIKit.h>
#import "TapDelegate.h"
#import "CollapsableTableViewTapRecognizer.h"
@interface CollapsableTableViewHeaderViewController : UIViewController
{
IBOutlet UILabel *collapsedIndicatorLabel,*titleLabel,*detailLabel;
CollapsableTableViewTapRecognizer* tapRecognizer;
BOOL viewWasSet;
id<TapDelegate> tapDelegate;
NSString* fullTitle;
BOOL isCollapsed;
}
@property (nonatomic, retain) NSString* fullTitle;
@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) BOOL isCollapsed;
@end
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. titleLabel
is the label containing the text of the header and 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) view:(UIView*) view tappedWithIdentifier:(NSString*) identifier;
@end
The view:tappedWithIdentifier:
selector is called when a header view is tapped andCollapsableTableView
implements the TapDelegate
protocol so that it can collapse or expand the corresponding header in response to this. When the selector is called, it will be called with the header view for theview
parameter and the title string of the header for the identifier
parameter, so that theCollapsableTableView
can do a look-up to determine whether the header is currently collapsed or not, and what its current section index is.
In the first published implementation of this project, this selector was called by theCollapsableTableViewHeaderViewController
of the corresponding header view. That worked because in that version the CollapsableTableView
stored (and thus retained) theCollapsableTableViewHeaderViewController
s of all of its sections. However, to make the implementation more memory-efficient - especially for tables with many sections - the CollapsableTableView
was changed so that it no longer does this. As a result, it turns out that the CollapsableTableViewHeaderViewController
of a header view is released from memory shortly after the header view appears in the table (the header UIView
still remains in memory as long as it's visible in the table). This means that when the header view is tapped there will probably be no CollapsableTableViewHeaderViewController
to call the TapDelegate
selector.
Before we look for a solution to this problem, let's see how the tap of a header view was previously detected and handled in CollapsableTableViewHeaderViewController.m.
- (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];
}
So we had overridden the setView:
method of the UIViewController
class in order to add aUITapGestureRecognizer
to the UIView
that is assigned to theCollapsableTableViewHeaderViewController
. This UITapGestureRecognizer
is configured to call a method of the CollapsableTableViewHeaderViewController
whenever the header view is tapped. In the new code this technique no longer works since the CollapsableTableViewHeaderViewController
will often have been deallocated by the time the user taps a header.
The one solution to this problem that is probably the most obvious is to configure the UITapGestureRecognizer
to call a selector in an object that will not have been deallocated when the user taps the header view. Some choices for this object are:
CollapsableTableView
UIView
UITapGestureRecognizer
The second choice will not work, since we don't have any control over where the UIView
that is passed into thesetView:
method comes from (that is, we cannot sub-class UIView
in order to add an extra method to it; perhaps we could wrap the passed-in UIView
object in an instance of a sub-class of UIView
of our own, but let's not go there!). Adding a method to CollapsableTableView
is an option, although adding a method with no parameters will not do, since the CollapsableTableView
would not know which header has been tapped. However, in the documentation of UITapGestureRecognizer
, we see that the alternative selector-type is a selector that takes theUITapGestureRecognizer
object as a parameter. However, we would have to sub-classUITapGestureRecognizer
in order to add a property that stores the title string of the header. So if we must sub-class UITapGestureRecognizer
, it would probably be more elegant to go with the third option and configure theUITapGestureRecognizer
to call a selector within itself. This is the approach taken in the implementation: we use a sub-class of UITapGestureRecognizer
, which we call CollapsableTableViewTapRecognizer
, and define it like this:
#import <Foundation/Foundation.h>
#import "TapDelegate.h"
@interface CollapsableTableViewTapRecognizer : UITapGestureRecognizer
{
id<TapDelegate> tapDelegate;
NSString* fullTitle;
UIView* tappedView;
}
@property (nonatomic, assign) id<TapDelegate> tapDelegate;
@property (nonatomic, retain) NSString* fullTitle;
@property (nonatomic, assign) UIView* tappedView;
- (id) initWithTitle:(NSString*) theFullTitle andTappedView:(UIView*)
theTappedView andTapDelegate:(id<TapDelegate>) theTapDelegate;
@end
Within the initWithTitle:andTappedView:andTapDelegate:
method, theCollapsableTableViewTapRecognizer
object is configured to call the private method headerTapped
when the view is tapped.
- (void) headerTapped
{
[tapDelegate view:tappedView tappedWithIdentifier:fullTitle];
}
Let's return to CollapsableTableView
now. When it gets a header title of a section from the client, it needs to be able to do a look-up to see if the header is collapsed, and what the section index of the header is. For this, we maintain two separate NSMutableDictionary
objects: one that maps a header title to a boolean value indicating if the header is collapsed, and one that maps a header title to an integer giving the section index of the header. 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,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 thetableView:numberOfRowsInSection:
method. It must also return a view of aCollapsableTableViewHeaderViewController
for each section, so it must also intercept calls to thetableView:viewForHeaderInSection:
method. So in order for CollapsableTableView
to be able to respond to both these selectors, it must implement the UITableViewDelegate
and theUITableViewDataSource
protocols and at run-time set its delegate and data source properties to... 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"
#define COLLAPSED_INDICATOR_LABEL_TAG 36
@interface CollapsableTableView :
UITableView <UITableViewDelegate,UITableViewDataSource,TapDelegate>
{
id<UITableViewDelegate> realDelegate;
id<UITableViewDataSource> realDataSource;
int toggleHeaderIdx;
NSMutableDictionary *headerTitleToIsCollapsedMap,
*headerTitleToSectionIdxMap,*sectionIdxToHeaderTitleMap;
}
@property (nonatomic,readonly) NSDictionary* headerTitleToIsCollapsedMap;
- (void) setIsCollapsed:(BOOL) isCollapsed forHeaderWithTitle:(NSString*) headerTitle;
- (void) setIsCollapsed:(BOOL) isCollapsed forHeaderWithTitle:(NSString*)
headerTitle andView:(UIView*) headerView;
- (void) setIsCollapsed:(BOOL) isCollapsed forHeaderWithTitle:(NSString*)
headerTitle withRowAnimation:(UITableViewRowAnimation) rowAnimation;
- (void) setIsCollapsed:(BOOL) isCollapsed forHeaderWithTitle:(NSString*)
headerTitle andView:(UIView*) headerView
withRowAnimation:(UITableViewRowAnimation) rowAnimation;
@end
The implementation of CollapsableTableView
pretty much follows from the discussion up to this point.
The setIsCollapsed:forHeaderWithTitle:...
methods are to be used to programmatically collapse or expand sections. If the client has a reference to the UIView
of the corresponding header, it can call one of the methods containing andView:
with that UIView
as parameter. If any one of the two other methods are called, the corresponding section (and header view) will have to be reloaded and the animation will sometimes be less nice than when one of the ...andView:
methods are used.
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 - 56.6 KB).
CollapsableTableView
can be used exactly like a regular UITableView
, as long as the client implements thetableView: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
in the xib toCollapsableTableView
. 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 oftableView: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.
If the client returns views for some cells, those views can contain a label that indicates if the header is collapsed. Simply set the tag
property of that label to the value COLLAPSED_INDICATOR_LABEL_TAG
as defined inCollapsableTableView.h. The CollapsableTableView
will then set the text of that label to '-' or '+' whenever the section is collapsed or expanded.
The client of the CollapsableTableView
can be unaware of the fact that it is not working with a regularUITableView
, but if it does know that the UITableView
is a CollapsableTableView
, it can cast the object to the latter type and use the headerTitleToIsCollapsedMap
property to determine which sections are collapsed and the setIsCollapsed:forHeaderWithTitle:
methods 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, intableView:titleForHeaderInSection:
, return a string of the form "Header Text|Detail Text".