iOS开发:多级列表的一种实现思路

去年项目改版前有个页面需要一个二级列表,本来想做出一个多级列表demo出来,但是当时因为时间比较赶就没有施行。最近正好有同学问起来,就抽时间来完成下这个拖了这么久的想法。
先放一下粗略的效果图(主要提供一下思路,如果使用的话得根据需求自己来进行修改了)和链接DEMO链接

多级列表效果图.gif

当时写二级列表的时候使用的是UITableView的headView和分组来实现的,但是因为这次想要实现的是一个多级列表,并不确定是多少级,可能是三级、四级甚至是十级(当然移动端不太可能出现这么多级的列表),想来想去使用headView好像都满足不了这个需求(可能是我的思维局限)。思考了一下觉得不使用headView,只要处理好模型的结构和源数据的管理问题,应该是可以实现的。

下面来说一下大概的思路:
首先创建UITableView必不可少,在不使用headView的情况下数据的结构就显得尤为重要。
多级列表大概的数据结构应该是这样的:



由图中不难看出,每一个节点都有名字,每一个节点都有可能会包含若干的子节点,并且每个节点会有两个:状态展开或者关闭(当然这是最基本的,可能实际上会需要更多的属性)。我们根据这些必须属性来建立model,代码如下

@interface TableViewModel : NSObject
//数据名称
@property (copy, nonatomic) NSString *name;
//状态 YES展开、NO收起
@property (assign, nonatomic) BOOL open;
//子节点
@property (strong, nonatomic) NSMutableArray *array;
@end

有了最基本的model以后,我们先来构造一组四层的假数据(用递归也可以,这里我就偷懒硬写了四层)。

//构造模拟数据
- (void)initData {
    //构造父节点
    for (int i = 0; i<5; i++) {
        TableViewModel *model = [[TableViewModel alloc]init];
        model.name = [NSString stringWithFormat:@"%i",i];
        model.array = [NSMutableArray array];
        //构造子节点
        for (int j = 6; j<10; j++) {
            TableViewModel *childModel = [[TableViewModel alloc]init];
            childModel.name = [NSString stringWithFormat:@"  %i",j];
            childModel.array = [NSMutableArray array];
            //构造孙子节点
            for (int k = 11; k<14; k++) {
                TableViewModel *grandsonModel = [[TableViewModel alloc]init];
                grandsonModel.name = [NSString stringWithFormat:@"      %i",k];
                grandsonModel.array = [NSMutableArray array];
                //构造曾孙节点
                for (int l = 15; l<17; l++) {
                    TableViewModel *grandsonSonModel = [[TableViewModel alloc]init];
                    grandsonSonModel.name = [NSString stringWithFormat:@"          %i",l];
                     //构造曾孙节点
                    [grandsonModel.array addObject:grandsonSonModel];
                }
                //孙子节点
                [childModel.array addObject:grandsonModel];
            }
            //子节点
            [model.array addObject:childModel];
        }
        //父节点
        [self.countArray addObject:model];
    }
}

构造完成后我们可以得到一个数组,这个数组里面的结构是这样的:


数组的结构

数据构造完成以后的任务就是显示了,那么怎么才能把这些数据按照我们要求的形式展现出来呢?
在不使用headView的情况下,tableView基本上就相当于一个分组。那么在控制器中建立一个数组,这个数组用来存储将要显示在tableView上的数据。只需管理这个数组中元素的个数与顺序就可以实现我们的需求。

接着创建一个可变数组用来管理要显示的数据。

@property (strong, nonatomic) NSMutableArray *countArray;

可以注意到,在构建数据的时候我首先将父节点加入到了这个数组中。将这些节点加入数组中后,将countArray设为UITableView的数据源,此时显示为父节点。

有了基本的思路,接下来就要开始考虑点击展开和点击收起需要怎么实现了。
我们先来考虑点击展开的问题:
当点击tableView时,我们需要获得当前点击项下所有的未展开节点、已展开节点、以及展开节点的子节点。因为不晓得该节点下存在多少层子节点所以这里需要用到递归来取出子节点(有点像遍历链表的感觉),取出子节点之后相应的按照顺序(这里创建了属性rowCount来记录点击行的下标,文章最后会说明为什么不使用参数传入。)加入到countArray中,然后刷新tableView将节点显示出来。代码如下:

//展开所有子节点
- (void)insertData:(NSMutableArray *)array {
    for (int i = 0; i<array.count; i++) {
        TableViewModel *model = [array objectAtIndex:i];
        self.rowCount++;
        [self.countArray insertObject:model atIndex:self.rowCount];
        if (model.array && model.open) {
            [self insertData:model.array];
        }
    }
}

有了展开,那么接下来就需要写关闭了。点击关闭时,同样的需要将所有的子节点全部从countArray中删除,具体方法和思路跟点击展开基本相同。代码如下:

//收起所有子节点
- (void)deleteData:(NSMutableArray *)array {
    for (int i = 0; i<array.count; i++) {
        TableViewModel *model = [array objectAtIndex:i];
        if (model.array) {
            [self deleteData:model.array];
        }
        [self.countArray removeObject:model];
    }  
}

至此,多级列表就完成了。
下面是同学要的另外一个小的附加功能,也顺便来介绍下实现思路。
大家应该看到了,刚开始放的图上面还有一个红色的浮窗,下面来介绍下这个浮窗的实现思路。
同学给的红色悬浮框的需求那,是当列表滑动时将将要显示的并且是展开状态分组的根节点,显示为悬浮框展示出来。那么首先通过看UITableViewDelegate中的API,发现里面有两个可以拿来使用的方法。

//获取到将要显示的cell
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath ;
//获取到消失的cell
- (void)tableView:(UITableView *)tableView didEndDisplayingCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath*)indexPath NS_AVAILABLE_IOS(6_0);

根据需求我们只需要屏幕上部cell的显示和消失情况,那么可以知道当用户向上滑动时,我们应该获取的为将要消失的cell;当用户将要向下滑动时,我们需要获取将要显示的cell。那么我们就需要监听tableView的滑动方向。
方法如下


-(void)scrollViewDidScroll:(UIScrollView *)scrollView {
     //如果当前位移大于缓存位移,说明scrollView向上滑动
    //state 0初始 1下 2上
    if (scrollView.contentOffset.y > oldY) {
        state = 2;
    }else{
        state = 1;
    }
    //将当前位移变成缓存位移
    oldY = scrollView.contentOffset.y;
    //当用户滑至顶部时,需要隐藏悬浮框
    if (scrollView.contentOffset.y <= 0) {
        self.label.hidden = YES;
    }
}

当用户向上滑动时,顶部cell在不断消失,我们只需拿到消失的cell所对应的model,当model展开状态时,将它显示出来即可。代码如下:

   TableViewModel *model = [self.countArray objectAtIndex:indexPath.row];
    if(model.open) {
          self.label.hidden = NO;
          self.label.text = model.name
    }

当用户向下滑动时,顶部cell在不断的显示出来,我们这个时候需要倒序遍历模型数组,拿到最近的一个展开状态的model,将它显示出来即可。如果遍历结束没有找到展开状态的model,那么将悬浮框隐藏。代码如下:

 for (NSInteger i = indexPath.row ; i>=0; i--) {
            TableViewModel *model = [self.countArray objectAtIndex:i];
            if (model.open) {
                self.label.hidden = NO;
                self.label.text = model.name;
                return;
            }
  }
  self.label.hidden = YES;

此时,基本已经完工了。但是如果去使用的时候会发现,悬浮框显示状态时,再点击展开列表,那么悬浮框上的数字会发生改变。原因是因为每次点击展开后,我会将数据相应的加入相应的数组然后刷新tableView,这个时候是会进入上面两个tableView的代理方法的,所以就会出现显示错乱的问题。此时,变量state就又起到了作用,只需在每次刷新时重置状态,并且在相应位置加上判断即可避免这种情况。同样的收起tableView的时候,只需在scrollViewDidScroll中进行判断即可。

如有错误欢迎大家指正。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,311评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,339评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,671评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,252评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,253评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,031评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,340评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,973评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,466评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,937评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,039评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,701评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,254评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,259评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,497评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,786评论 2 345

推荐阅读更多精彩内容

  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,024评论 4 62
  • 百花诗 100 百日草 一茎圆叶双双绿,五彩娇花步步高。 秋艳春红争日月,天长地久共逍遥。
    PikeTalk阅读 649评论 0 3
  • 我是一名大四的历史学专业的学生,近期在准备考研,所以整天沉浸在密密麻麻的专业书里,非常枯燥。今天突然有个想法就...
    jc一蓑烟雨阅读 975评论 0 8
  • 余近數月之反省錄,實無反省。此有乖余立此博客之初旨,或為每日都記此日記之故也。昨日與人閒談,同席者曰余有救世主心態...
    寒窗寄傲生阅读 210评论 0 0