美文网首页
iOS实现折叠/展开 UITableView 的简单思路

iOS实现折叠/展开 UITableView 的简单思路

作者: Ziggear | 来源:发表于2024-06-21 15:12 被阅读0次

因为最近需要在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 的无限层级展开。

demo.gif

篇幅原因这里放不下所有实现代码,我已经将一个完整的实现放出,如果有需要参考的可以移步:https://github.com/ziggear/ZGExpandableTable

参考资料:
省市数据:https://github.com/wecatch/china_regions

相关文章

网友评论

      本文标题:iOS实现折叠/展开 UITableView 的简单思路

      本文链接:https://www.haomeiwen.com/subject/tnwcyrtx.html