IOS tree multi-level menu

Realization renderings

Without further ado, let’s get straight to the code;
Create an entity class to store row data
MenuNode.h

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface MenuNode: NSObject
@property (nonatomic, copy) NSString * parentId;//The id of the parent node, if it is -1, it means that the node is the root node

@property (nonatomic, copy) NSString * nodeId;//The id of this node

@property (nonatomic, copy) NSString *name;//The name of this node

@property (nonatomic, copy) NSString *code;//Other parameters

@property (nonatomic, copy) NSString *other;//Other parameters

@property (nonatomic, assign) int depth;//The depth of the node

@property (nonatomic, assign) int sortNo;//The sequence number of the node

@property (nonatomic, assign) BOOL isParent;//Whether this node has child nodes

@property (nonatomic, assign) BOOL childIsExpand;//Whether the child nodes of this node are expanded

@property (nonatomic, assign) BOOL expand;//Whether the node is in the expanded state
@end

NS_ASSUME_NONNULL_END

MenuNode.m

#import "MenuNode.h"

@implementationMenuNode
@end

Picture resources
Default collapse icon
Expand icon
Default radio button
Select the radio button
Collapse icon
Expand icon
Multiple selection button
Select the multi-select button
TreeMenuView.h

#import <UIKit/UIKit.h>


NS_ASSUME_NONNULL_BEGIN

@class MenuNode;

@protocol TreeMenuCellDelegate <NSObject>

-(void)cellClick : (MenuNode *)node;

-(void)chooseDataChange:(NSDictionary *)chooseData;

@end

@interface TreeMenuView : UITableView

@property (nonatomic, weak) id<TreeMenuCellDelegate> treeMenuCellDelegate;

@property (nonatomic, strong) NSMutableDictionary *chooseData;//Used to store selected data;

@property (nonatomic,assign) BOOL *isRadio; // Whether to select multiple times;

typedef void (^treeMenuBlock)(NSMutableDictionary *);

@property (nonatomic,copy) treeMenuBlock block;

-(instancetype)init;

-(instancetype)initWithFrame:(CGRect)frame Data:(NSArray *)data ChooseData:(NSMutableDictionary *)chooseData;

-(void)reloadData : (NSArray *)data ChooseData:(NSMutableDictionary *) chooseData;

-(void)reloadData : (NSArray *)data;

-(void)clearRadio;

-(void)unExpandAll;

@end

NS_ASSUME_NONNULL_END

TreeMenuView.m

#import "TreeMenuView.h"
#import "MenuNode.h"
@interface TreeMenuView ()<UITableViewDataSource,UITableViewDelegate>

@property (nonatomic, strong) NSArray *data;//The organized data is passed in (full data)

@property (nonatomic, strong) NSMutableArray *tempData;//Used to store data source (partial data)


@end

@implementation TreeMenuView{<!-- -->
    MenuNode *node;
    MenuNode *parentNode;
}
-(instancetype)init{<!-- -->
    self=[super init];
    if (self) {<!-- -->
        self.dataSource = self;
        self.delegate = self;
        _data= [NSMutableDictionary dictionary];
        _tempData = [self createTempData:_data];
        _chooseData= [NSMutableDictionary dictionary];
        _isRadio=YES;//Default radio selection;
    }
    return self;
}

-(instancetype)initWithFrame:(CGRect)frame Data:(NSArray *)data ChooseData:(NSMutableDictionary *)chooseData{<!-- -->
    self = [super initWithFrame:frame style:UITableViewStyleGrouped];
    if (self) {<!-- -->
        self.dataSource = self;
        self.delegate = self;
        if(data)
            _data = data;
        else
            _data= [NSMutableDictionary dictionary];
        _tempData = [self createTempData:data];
        if(chooseData)
            _chooseData=chooseData;
        else
        _chooseData= [NSMutableDictionary dictionary];
        _isRadio=YES;//Default radio selection;
    }
    return self;
}

/**
 * Initialize data source
 * The default expansion is that the display/level is 1
 */
-(NSMutableArray *)createTempData : (NSArray *)data{<!-- -->
    NSMutableArray *tempArray = [NSMutableArray array];
    
    for (int i=0; i<data.count; i + + ) {<!-- -->
        MenuNode *node = [_data objectAtIndex:i];
        if (node.expand) {<!-- -->
            [tempArray addObject:node];
        }
    }
    if(tempArray.count==0){<!-- -->
        for (int i=0; i<data.count; i + + ) {<!-- -->
            MenuNode *node = [_data objectAtIndex:i];
            if (node.depth==1) {<!-- -->
                [tempArray addObject:node];
            }
        }
    }
    return tempArray;
}
-(void)reloadData : (NSArray *)data{<!-- -->
         _data = data;
         _tempData = [self createTempData:data];
    [self reloadData];
}
-(void)reloadData : (NSArray *)data ChooseData:(NSMutableDictionary *) chooseData{<!-- -->
    _data = data;
    if(chooseData)
        _chooseData=chooseData;
    else
        _chooseData= [NSMutableDictionary dictionary];
    _tempData = [self createTempData:data];
    [self reloadData];
}

-(void)clearRadio{<!-- -->
    _chooseData= [NSMutableDictionary dictionary];
    [self reloadData];
}
-(void)unExpandAll{<!-- -->
    for (int i=0; i<_data.count; i + + ) {<!-- -->
        node = [_data objectAtIndex:i];
        node.expand = NO;
    }
}
#pragma mark - UITableViewDataSource

#pragma mark-Required

-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{<!-- -->
    return _tempData.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{<!-- -->
    static NSString *NODE_CELL_ID = @"node_cell_id";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NODE_CELL_ID];
    node = [_tempData objectAtIndex:indexPath.row];
    if(!cell){<!-- -->
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NODE_CELL_ID];
    }
    for (UIView *view in [cell.contentView subviews]) {<!-- -->
        [view removeFromSuperview];
    }
    if(node.isParent){<!-- -->
        UIImageView *img=[[UIImageView alloc] initWithFrame:CGRectMake(15*node.depth,12,16,16)];
        if(node.expand)
        {<!-- -->
            [img setImage:[UIImage imageNamed:@"caret-down.png"]];
        }
        else
        {<!-- -->
            [img setImage:[UIImage imageNamed:@"caret-right.png"]];
        }
        [cell.contentView addSubview:img];
    }
    UIButton *CheckButton=[[UIButton alloc] initWithFrame:CGRectMake(15*node.depth + 20,10,20,20)];
    [CheckButton setTitleColor:[UIColor colorWithRed:7.0f/255.0f green:50.0f/255.0f blue:80.0f/255.0f alpha:1.0f] forState:UIControlStateNormal];
    [CheckButton setTitleColor:[UIColor colorWithRed:210.0f/255.0f green:210.0f/255.0f blue:210.0f/255.0f alpha:1.0f] forState:UIControlStateHighlighted];
    [CheckButton setTitleColor:[UIColor colorWithRed:210.0f/255.0f green:210.0f/255.0f blue:210.0f/255.0f alpha:1.0f] forState:UIControlStateSelected];
    if(_isRadio){<!-- -->
        [CheckButton setImage:[UIImage imageNamed:@"radiobox_unselected.png"] forState:UIControlStateNormal];
        [CheckButton setImage:[UIImage imageNamed:@"radiobox_selected.png"] forState:UIControlStateSelected];
    }else{<!-- -->
        [CheckButton setImage:[UIImage imageNamed:@"checkbox_unselected.png"] forState:UIControlStateNormal];
        [CheckButton setImage:[UIImage imageNamed:@"checkbox_selected.png"] forState:UIControlStateSelected];
    }
    NSString *nodeid=node.nodeId;
    if([_chooseData objectForKey:nodeid])
        CheckButton.selected=YES;
    else
        CheckButton.selected=NO;
    CheckButton.tag=indexPath.row;
    [CheckButton addTarget:self action:@selector(didSelected:) forControlEvents:UIControlEventTouchUpInside];
    [cell.contentView addSubview:CheckButton];
    
    UILabel *title=[[UILabel alloc] initWithFrame:CGRectMake(15*node.depth + 20 + 25,0,self.frame.size.width-(15*node.depth + 20 + 25),40)];
    title.numberOfLines=0;
    title.lineBreakMode=UILineBreakModeWordWrap;
    title.text=node.name;
    title.font=[UIFont systemFontOfSize:12.0f];
    [cell.contentView addSubview:title];
   
    
    UIControl *actionView=[[UIControl alloc] initWithFrame:CGRectMake( 15*node.depth + 20 + 25,0,self.frame.size.width-(15*node.depth + 20 + 25),40)];
    actionView.backgroundColor=[UIColor redColor];
    actionView.tag = indexPath.row;
    if([_chooseData objectForKey:nodeid])
        actionView.selected=YES;
    else
        actionView.selected=NO;
    
    actionView.backgroundColor = [UIColor clearColor];
    [actionView addTarget:self action:@selector(selectAction:) forControlEvents:UIControlEventTouchUpInside];
    
    NSDictionary *attrs = @{<!-- -->NSFontAttributeName:[UIFont systemFontOfSize:12.0f]};
    CGSize size=[node.name sizeWithAttributes:attrs];
    if(self.frame.size.width>=15*node.depth + 20 + 25 + size.width){<!-- -->
        actionView.frame = CGRectMake(15*node.depth + 20 + 25, 0, 20 + size.width, 40);
    }else{<!-- -->
        actionView.frame = CGRectMake(15*node.depth + 20 + 25, 0, self.frame.size.width-(15*node.depth + 20 + 25), 40);
    }
    
    [cell.contentView addSubview:actionView];
    
    
    return cell;
}
-(void)selectAction:(UIControl *)view{<!-- -->
    view.selected=!view.selected;
    int row=view.tag;
    MenuNode *node=[_tempData objectAtIndex:row];
    if (view.selected) {<!-- -->
        if(_isRadio)
        [_chooseData removeAllObjects];
        [_chooseData setObject:node forKey:node.nodeId];
    }else{<!-- -->
        [_chooseData removeObjectForKey:node.nodeId];
    }
    if(!_isRadio)//If it is not a single selection, select its child;
    for (int i=0; i<_data.count; i + + ) {<!-- -->
        MenuNode *cnode = [_data objectAtIndex:i];
        if (cnode.parentId!=[NSNull null] & amp; & amp;[cnode.parentId isEqualToString: node.nodeId ]) {<!-- -->
            if(view.selected)
                [_chooseData setObject:cnode forKey:cnode.nodeId];
            else{<!-- -->
                [_chooseData removeObjectForKey:cnode.nodeId];
            }
        }
    }
    if (_treeMenuCellDelegate & amp; & amp; [_treeMenuCellDelegate respondsToSelector:@selector(chooseDataChange:)]) {<!-- -->
        [_treeMenuCellDelegate chooseDataChange:_chooseData];
    }
    self.block(_chooseData);
    [self reloadData];
}
-(void)didSelected:(UIButton *)btn{<!-- -->
    btn.selected=!btn.selected;
    int row=btn.tag;
    MenuNode *node=[_tempData objectAtIndex:row];
    if (btn.selected) {<!-- -->
        if(_isRadio)
        [_chooseData removeAllObjects];
        [_chooseData setObject:node forKey:node.nodeId];
    }else{<!-- -->
        [_chooseData removeObjectForKey:node.nodeId];
    }
    if(!_isRadio)//If it is not a single selection, select its child;
    for (int i=0; i<_data.count; i + + ) {<!-- -->
        MenuNode *cnode = [_data objectAtIndex:i];
        if (cnode.parentId!=[NSNull null] & amp; & amp;[cnode.parentId isEqualToString: node.nodeId ]) {<!-- -->
            if(btn.selected)
                [_chooseData setObject:cnode forKey:cnode.nodeId];
            else{<!-- -->
                [_chooseData removeObjectForKey:cnode.nodeId];
            }
        }
    }
    if (_treeMenuCellDelegate & amp; & amp; [_treeMenuCellDelegate respondsToSelector:@selector(chooseDataChange:)]) {<!-- -->
        [_treeMenuCellDelegate chooseDataChange:_chooseData];
    }
    self.block(_chooseData);
    [self reloadData];
    
}

#pragma mark - Optional

- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section{<!-- -->
    return 0.01;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{<!-- -->
    return 40;
}

- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section{<!-- -->
    return 0.01;
}

#pragma mark-UITableViewDelegate

#pragma mark - Optional
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{<!-- -->
// NSLog(@"SelectRow:%@",_data);
    //Modify the data source first
    parentNode = [_tempData objectAtIndex:indexPath.row];
    if(parentNode.isParent){<!-- -->
        parentNode.expand=!parentNode.expand;
        NSUInteger startPosition = indexPath.row + 1;
        NSUInteger endPosition = startPosition;
        BOOL expand = NO;
        for (int i=0; i<_data.count; i + + ) {<!-- -->
            node = [_data objectAtIndex:i];
            if (node.parentId!=[NSNull null] & amp; & amp;[node.parentId isEqualToString: parentNode.nodeId]) {<!-- -->
// node.expand = !node.expand;
                node.depth = parentNode.depth + 1;
                if (parentNode.expand) {<!-- -->
                    [_tempData insertObject:node atIndex:endPosition];
                    expand = YES;
                    endPosition + + ;
                }else{<!-- -->
                    expand = NO;
                    endPosition = [self removeAllNodesAtParentNode:parentNode];
                    break;
                }
            }
        }
    // NSLog(@"SelectRow:%@",_tempData);
        //Get the indexPath that needs to be corrected
        NSMutableArray *indexPathArray = [NSMutableArray array];
        for (NSUInteger i=startPosition; i<endPosition; i + + ) {<!-- -->
            NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
            [indexPathArray addObject:tempIndexPath];
        }
        
        //Insert or delete related nodes
        if (expand) {<!-- -->
            [self insertRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
        }else{<!-- -->
            [self deleteRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
        }
    }else{<!-- -->
        if (_treeMenuCellDelegate & amp; & amp; [_treeMenuCellDelegate respondsToSelector:@selector(cellClick:)]) {<!-- -->
            [_treeMenuCellDelegate cellClick:parentNode];
        }
    }
    [self reloadData];
    
    
}

/**
 * Delete all child nodes under the parent node (including grandchild nodes)
 *
 * @param parentNode parent node
 *
 * @return The position of the next adjacent node of the same level under the parent node
 */
-(NSUInteger)removeAllNodesAtParentNode : (MenuNode *)parentNode{<!-- -->
    NSUInteger startPosition = [_tempData indexOfObject:parentNode];
    NSUInteger endPosition = startPosition;
    for (NSUInteger i=startPosition + 1; i<_tempData.count; i + + ) {<!-- -->
        node = [_tempData objectAtIndex:i];
        endPosition + + ;
        if (node.depth <= parentNode.depth) {<!-- -->
            break;
        }
        if(endPosition == _tempData.count-1){<!-- -->
            endPosition + + ;
            node.expand = NO;
            break;
        }
        node.expand = NO;
    }
    if (endPosition>startPosition) {<!-- -->
        [_tempData removeObjectsInRange:NSMakeRange(startPosition + 1, endPosition-startPosition-1)];
    }
    return endPosition;
}

@end

use

 TreeMenuView *areaMenu = [[TreeMenuView alloc] init];
    areaMenu.layer.cornerRadius=8;
    areaMenu.estimatedRowHeight = 0;
    areaMenu.estimatedSectionHeaderHeight = 0;
    areaMenu.estimatedSectionFooterHeight = 0;
    areaMenu.isRadio=YES;
    areaMenu.block = ^(NSMutableDictionary *chooseData) {<!-- -->//Select callback
        
    };

data binding

 MenuNode *ntmp = [[MenuNode alloc] init];
[areaListData addObject:ntmp];
[areaMenu reloadData:areaListData ChooseData:areaChooseData];