iOS可重复使用的折叠式表视图

来源:互联网 发布:用手机怎么找淘宝小二 编辑:程序博客网 时间:2024/04/30 12:37

我曾经在iPhone上的应用,显示了大量投入,分为 ​​不同的类别,UITableView的工作。要改变值的输入,用户按表视图中相应的行之一,并在一个单独的屏幕上出现的改变值。表视图每个类别的一个部分,每个部分包含每个输入表格单元格(行)。

问题是,投入的数量变得非常,非常大,因此,它并没有给用户一个很好的概述。这是从表上方滚动的底部甚至乏味。

我们决定,用户应该能够折叠和展开只需按节头表的部分(类)。我们需要的代码实现这个应该是可重复使用的,它需要修改现有的代码尽可能少的数量。

下面的截图显示的表视图,其可折叠的部分,看起来像。

collapsable_table_view.png

实施

我想,实现上述目标的最佳途径是创建一个子类的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) theCollapsableTableViewHeaderViewControllers 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 UITapGestureRecognizerto 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 CollapsableTableViewstores 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 tagproperty 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".

原创粉丝点击