iOS開發UI篇——一個可擴充套件性極強的樹形控制元件

邏輯教育-楚陽發表於2018-11-14

一、簡介

樹形控制元件在多列列表、多級選單中使用比較常見,比如:國家-省份-城市 多級選擇、學校-專業-班級 多級選擇等等。然而IOS自帶控制元件中並不存在樹形控制元件,我們要在IOS開發中使用樹形控制元件,通常需要自己擴充套件UITableView列表控制元件。
現在在這裡開源一個自己寫的高擴充套件性,高複用性的IOS樹形結構控制元件。
支援無限極樹形結構。
使用的是非遞迴方式。
程式碼簡單易懂,擴充套件方便。
圖片演示如下:


iOS開發UI篇——一個可擴充套件性極強的樹形控制元件
image

二、使用說明

第一步:建立資料模型

parentId : 該節點的父控制元件id號,如果為-1則表示該節點為根節點
nodeId : 每個節點自身的id號,是每個節點的唯一標示
name : 節點的名稱
depth : 該節點所帶的樹形結構中的深度,根節點的深度為0
expand : 該節點是否處於展開狀態

/**
*  每個節點型別
*/
@interface Node : NSObject

@property (nonatomic , assign) int parentId;//父節點的id,如果為-1表示該節點為根節點

@property (nonatomic , assign) int nodeId;//本節點的id

@property (nonatomic , strong) NSString *name;//本節點的名稱

@property (nonatomic , assign) int depth;//該節點的深度

@property (nonatomic , assign) BOOL expand;//該節點是否處於展開狀態

/**
*快速例項化該物件模型
*/
- (instancetype)initWithParentId : (int)parentId nodeId : (int)nodeId name : (NSString *)name depth : (int)depth expand : (BOOL)expand;

@end
複製程式碼

第二步:按照以上的資料模型,組裝資料,下面以 國家-身份-城市 的三級目錄進行演示。

//----------------------------------中國的省地市關係圖3,2,1--------------------------------------------
Node *country1 = [[Node alloc] initWithParentId:-1 nodeId:0 name:@"中國" depth:0 expand:YES];
Node *province1 = [[Node alloc] initWithParentId:0 nodeId:1 name:@"江蘇" depth:1 expand:NO];
Node *city1 = [[Node alloc] initWithParentId:1 nodeId:2 name:@"南通" depth:2 expand:NO];
Node *city2 = [[Node alloc] initWithParentId:1 nodeId:3 name:@"南京" depth:2 expand:NO];
Node *city3 = [[Node alloc] initWithParentId:1 nodeId:4 name:@"蘇州" depth:2 expand:NO];
Node *province2 = [[Node alloc] initWithParentId:0 nodeId:5 name:@"廣東" depth:1 expand:NO];
Node *city4 = [[Node alloc] initWithParentId:5 nodeId:6 name:@"深圳" depth:2 expand:NO];
Node *city5 = [[Node alloc] initWithParentId:5 nodeId:7 name:@"廣州" depth:2 expand:NO];
Node *province3 = [[Node alloc] initWithParentId:0 nodeId:8 name:@"浙江" depth:1 expand:NO];
Node *city6 = [[Node alloc] initWithParentId:8 nodeId:9 name:@"杭州" depth:2 expand:NO];
//----------------------------------美國的省地市關係圖0,1,2--------------------------------------------
Node *country2 = [[Node alloc] initWithParentId:-1 nodeId:10 name:@"美國" depth:0 expand:YES];
Node *province4 = [[Node alloc] initWithParentId:10 nodeId:11 name:@"紐約州" depth:1 expand:NO];
Node *province5 = [[Node alloc] initWithParentId:10 nodeId:12 name:@"德州" depth:1 expand:NO];
Node *city7 = [[Node alloc] initWithParentId:12 nodeId:13 name:@"休斯頓" depth:2 expand:NO];
Node *province6 = [[Node alloc] initWithParentId:10 nodeId:14 name:@"加州" depth:1 expand:NO];
Node *city8 = [[Node alloc] initWithParentId:14 nodeId:15 name:@"洛杉磯" depth:2 expand:NO];
Node *city9 = [[Node alloc] initWithParentId:14 nodeId:16 name:@"舊金山" depth:2 expand:NO];

//----------------------------------日本的省地市關係圖0,1,2--------------------------------------------
Node *country3 = [[Node alloc] initWithParentId:-1 nodeId:17 name:@"日本" depth:0 expand:YES];
NSArray *data = [NSArray arrayWithObjects:country1,province1,city1,city2,city3,province2,city4,city5,province3,city6,country2,province4,province5,city7,province6,city8,city9,country3, nil];
複製程式碼

第三步:使用以上資料進行TeeTableView的初始化。

1.TreeTableView *tableview = [[TreeTableView alloc] initWithFrame:CGRectMake(0, 20, CGRectGetWidth(self.view.frame), CGRectGetHeight(self.view.frame)-20) withData:data];
[self.view addSubview:tableview];
複製程式碼

通過簡單以上三步,你就可以把該樹形控制元件整合到你的專案中。

三、實現原理

樹形結構的列表用的其實就是UITableView控制元件,但是如何能夠讓UItableView能夠動態的增加和刪除指定的行數的cell是實現樹形結構的關鍵所在。
這時候我們需要用到兩個UItableView自帶的行數:

- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
複製程式碼

第一個函式用來在指定的位置插入cells,第二個函式用來在指定的位置刪除cells,並且這二個函式都自帶多種動畫效果,讓刪除和插入的過程不至於太突兀、有種漸變的感覺,具有良好的使用者體驗。
對於這幾個動畫做了嘗試:
UITableViewRowAnimationFade : 漸變效果
UITableViewRowAnimationRight : 右邊進入,右邊消失
UITableViewRowAnimationLeft : 左邊進入,左邊消失
UITableViewRowAnimationTop : 頂部進入,頂部消失
UITableViewRowAnimationBottom : 頂部進入,底部消失

注意點:

在呼叫insertRowsAtIndexPaths和deleteRowsAtIndexPaths的時候一定要先改變資料來源,在呼叫上述函式,不然會產生crash。

接下來把TreeTableView的主要程式碼展示出來,因為本來程式碼量就不大,而且程式碼中註釋也比較全,希望能夠幫助大家理解。

#import "TreeTableView.h"
#import "Node.h"

@interface TreeTableView ()<UITableViewDataSource,UITableViewDelegate>

@property (nonatomic , strong) NSArray *data;//傳遞過來已經組織好的資料(全量資料)

@property (nonatomic , strong) NSMutableArray *tempData;//用於儲存資料來源(部分資料)


@end

@implementation TreeTableView

-(instancetype)initWithFrame:(CGRect)frame withData : (NSArray *)data{
    self = [super initWithFrame:frame style:UITableViewStyleGrouped];
    if (self) {
        self.dataSource = self;
        self.delegate = self;
        _data = data;
        _tempData = [self createTempData:data];
    }
    return self;
}

/**
 * 初始化資料來源
 */
-(NSMutableArray *)createTempData : (NSArray *)data{
    NSMutableArray *tempArray = [NSMutableArray array];
    for (int i=0; i<data.count; i++) {
        Node *node = [_data objectAtIndex:i];
        if (node.expand) {
            [tempArray addObject:node];
        }
    }
    return tempArray;
}


#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];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:NODE_CELL_ID];
    }

    Node *node = [_tempData objectAtIndex:indexPath.row];

    NSMutableString *name = [NSMutableString string];
    for (int i=0; i<node.depth; i++) {
        [name appendString:@"     "];
    }
    [name appendString:node.name];

    cell.textLabel.text = name;

    return cell;
}


#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{
    //先修改資料來源
    Node *parentNode = [_tempData objectAtIndex:indexPath.row];
    NSUInteger startPosition = indexPath.row+1;
    NSUInteger endPosition = startPosition;
    BOOL expand = NO;
    for (int i=0; i<_data.count; i++) {
        Node *node = [_data objectAtIndex:i];
        if (node.parentId == parentNode.nodeId) {
            node.expand = !node.expand;
            if (node.expand) {
                [_tempData insertObject:node atIndex:endPosition];
                expand = YES;
            }else{
                expand = NO;
                endPosition = [self removeAllNodesAtParentNode:parentNode];
                break;
            }
            endPosition++;
        }
    }

    //獲得需要修正的indexPath
    NSMutableArray *indexPathArray = [NSMutableArray array];
    for (NSUInteger i=startPosition; i<endPosition; i++) {
        NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
        [indexPathArray addObject:tempIndexPath];
    }

    //插入或者刪除相關節點
    if (expand) {
        [self insertRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
    }else{
        [self deleteRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationNone];
    }
}

/**
 *  刪除該父節點下的所有子節點(包括孫子節點)
 *
 *  @param parentNode 父節點
 *
 *  @return 鄰接父節點的位置距離該父節點的長度,也就是該父節點下面所有的子孫節點的數量
 */
-(NSUInteger)removeAllNodesAtParentNode : (Node *)parentNode{
    NSUInteger startPosition = [_tempData indexOfObject:parentNode];
    NSUInteger endPosition = startPosition;
    for (NSUInteger i=startPosition+1; i<_tempData.count; i++) {
        Node *node = [_tempData objectAtIndex:i];
        endPosition++;
        if (node.depth == parentNode.depth) {
            break;
        }
        node.expand = NO;
    }
    if (endPosition>startPosition) {
        [_tempData removeObjectsInRange:NSMakeRange(startPosition+1, endPosition-startPosition-1)];
    }
    return endPosition;
}
複製程式碼

四、總結

在演示專案中,每個cell我都使用系統自帶的cell,樣式比較簡單,如果你要展現更加漂亮的樣式,可以自定義cell。
同時,你也可以擴充套件該資料模型,運動到更加複雜的業務處理中。比如以下場景:


iOS開發UI篇——一個可擴充套件性極強的樹形控制元件
2.gif

五、下載地址

Demo下載地址:這是一個我的iOS交流群:624212887,群檔案自行下載,不管你是小白還是大牛熱烈歡迎進群 ,分享面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路。

如果覺得對你還有些用,就關注小編+喜歡這一篇文章。你的支援是我繼續的動力。

下篇文章預告:iOS開發UI篇--一個支援圖文混排的ActionSheet

文章來源於網路,如有侵權,請聯絡小編刪除。


相關文章