因为最近需要在iOS设备上展示一些可以折叠/展开的数据,比如用 UITableView 展示省市数据或JSON数据 ,首先想到的就是用一个UITableView作为容器来存放,再通过一些定制的方法例如自定义可展开的 UITableViewCell 来实现数据的展开、折叠显示。本文源码可以到这里 ZGExpandableTable查看
但在实际开发过程中,我发现用cell自定义的方式来实现还是过于复杂,因此总结两种 UITableView 的方便用于数据展开的方法
1 一层展开
代码设计思路
一层展开有很多种方法实现,我认为最快也是最方便维护的方式是利用 UITableView 的 section 机制来实现,即用 section 展示一级数据,用 cell 展示二级数据,再维护一个数组用来表示第一级数据是否展开即可
具体实现方法
具体的实现以【显示全国各省市数据列表】为例。(数据来源可以参考文后的参考链接)。首先准备好三份数据,分别是一级数据(省份,province.json)、二级数据(城市,city.json)、一级的展开情况:
@property (nonatomic, strong) NSArray *provinces;
@property (nonatomic, strong) NSDictionary *cities;
@property (nonatomic, strong) NSMutableArray *provincesExpanded;
然后在TableView 的 DataSource 中处理 section,将一级数据在 section 中展示,这里添加了一层 UIButton 到 section header 是因为 UITableView 的 section header 没有自带的点击回调:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.provinces.count;
}
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {
NSDictionary *prov = self.provinces[section];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(10, 0, tableView.frame.size.width - 20, 55)];
label.text = prov[@"name"];
UIButton *button = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, tableView.frame.size.width , 55)];
[button addSubview:label];
[button addTarget:self action:@selector(handleSectionHeaderClick:) forControlEvents:UIControlEventTouchUpInside];
button.tag = section;
return button;
}

当用户点击了省份后,我们用 handleSectionHeaderClick: 方法处理展开的情况:
- (void)handleSectionHeaderClick:(UIButton *)btn {
BOOL expanded = [self.provincesExpanded[btn.tag] boolValue];
[self.provincesExpanded replaceObjectAtIndex:btn.tag withObject:@(!expanded)];
[self.tableView reloadData];
}
对应的,实现row的dataSource:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
BOOL expanded = [self.provincesExpanded[section] boolValue];
if (expanded) {
NSDictionary *prov = self.provinces[section];
NSArray *citiesOfProv = self.cities[prov[@"id"]];
return citiesOfProv.count;
} else {
return 0;
}
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
}
NSDictionary *prov = self.provinces[indexPath.section];
NSArray *citiesOfProv = self.cities[prov[@"id"]];
NSString *cityName = citiesOfProv[indexPath.row][@"name"];
cell.textLabel.text = cityName;
return cell;
}
这样,在点击 section 后,就可实现各自 section 下 row 的展开、收起效果:

2 N层展开
设计思路
上面的方法利用了 UITableView 的 section 的特性,但是灵活度受限。因为:(1)由于 UITableView 的 section 机制决定只能实现一层展开,如果要实现多层展开又要回到本文开头的“定制化Cell”的思路中去了,不够优雅(2)如果你的UI设计中需要利用 section header 做一些别的事情,这两者就冲突了,不够灵活
如果需求是实现 省-市-县-乡 这种数据的展示,要怎么实现呢?再比如要展示一份JSON数据,是否可以根据数据无限的展开?下面有一份JSON数据的例子:
{
"glossary": {
"title": "example glossary",
"GlossDiv": {
"title": "S",
"GlossList": {
"GlossEntry": {
"ID": "SGML",
"SortAs": "SGML",
"GlossTerm": "Standard Generalized Markup Language",
"Acronym": "SGML",
"Abbrev": "ISO 8879:1986",
"GlossDef": {
"para": "A meta-markup language, used to create markup languages such as DocBook.",
"GlossSeeAlso": ["GML", "XML"]
},
"GlossSee": "markup"
}
}
}
}
}
因此我又想了一种递归+摊平的方式来实现。因为JSON数据解析到iOS运行时后一般是NSDictionary或 NSArray,像上面那样用一个数组来维护“折叠、展开”的状态会非常复杂。
实现方法
因此我需要一个类似树结构的节点来递归解析出等价的 NSDictionary或 NSArray,同时在这个节点中带上“是否展开”这一个属性:
@interface ZGJSONNode : NSObject
// empty for an array
@property (nonatomic, copy) NSString *key;
// may be string number and bool
@property (nonatomic, strong) id value;
// sub nodes
@property (nonatomic, strong) NSMutableArray <ZGJSONNode *>* children;
// expanded flag
@property (nonatomic, assign) BOOL expanded;
@end
然后就是递归解析的代码,和树的遍历类似:
- (ZGJSONNode *)parse:(id)jsonObject key:(NSString *)key {
ZGJSONNode *node = [[ZGJSONNode alloc] init];
node.key = key;
if (![jsonObject isKindOfClass:[NSDictionary class]] && ![jsonObject isKindOfClass:[NSArray class]]) {
node.value = jsonObject;
} else {
NSMutableArray *children = [NSMutableArray array];
// ...
// ... 这里判断是NSArray还是NSDictionay 分别递归进行解析
// ...
node.children = [children copy];
}
return node;
}
我们从文件或网络取到JSON数据后可以这样去调用:
NSError *err = nil;
id object = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&err];
if (err) {
return NO;
}
ZGJSONNode *root = [self parse:object key:nil];
self.jsonRoot = root;
接下来要考虑怎么展示了。因为 UITableView 设计的原因,最适合用来给 UITableView 做展示的数据结构是一维数组,因此我设计一个数据模型用于展示所有需要在 UITableView 上显示的数据(包括已经展开的数据):
@interface ZGJSONNodeShowModel : NSObject
@property (nonatomic, copy) NSString *text;
@property (nonatomic, weak) ZGJSONNode *node;
@end
这里我给每一个 ZGJSONNodeShowModel 对应了一个JSON数据节点ZGJSONNode,方便后续操作。
然后在需要展示数据时,用同样递归的方式对root node进行遍历,并将结果全部存到 ZGJSONNodeShowModel 一维数组中。再处理一下显示效果,例如子项在cell中显示时,可以加入一些缩进,这样就实现了基于 UITableView 的无限层级展开。

篇幅原因这里放不下所有实现代码,我已经将一个完整的实现放出,如果有需要参考的可以移步:https://github.com/ziggear/ZGExpandableTable
参考资料:
省市数据:https://github.com/wecatch/china_regions
网友评论