TreeTableViewWithSwift是用Swift編寫的樹形結(jié)構(gòu)顯示的TableView控件。
TreeTableViewWithSwift的由來(lái)
在開發(fā)企業(yè)通訊錄的時(shí)候需要層級(jí)展示。之前開發(fā)Android的時(shí)候有做過類似的功能,也是通過一些開源的內(nèi)容進(jìn)行改造利用。此次,在做ios的同類產(chǎn)品時(shí),調(diào)研發(fā)現(xiàn)樹形結(jié)構(gòu)的控件并不是很多,雖然也有但大多看起來(lái)都比較負(fù)責(zé),而且都是用OC編寫的。介于我的項(xiàng)目是Swift開發(fā)的,并且TreeTableView貌似沒有人用Swift編寫過(也可能是我沒找到)。所以打算自己動(dòng)手寫一個(gè),從而豐衣足食。
TreeTableViewWithSwift簡(jiǎn)介
開發(fā)環(huán)境:Swift 2.0,XCode版本:7.0.1,ios 9.0升級(jí)到 swift 3.0, Xcode 8.2.1
代碼:GitHub代碼
1、運(yùn)行效果

2、關(guān)鍵代碼的解讀
TreeTableViewWithSwift其實(shí)是對(duì)tableview的擴(kuò)展。在此之前需要先創(chuàng)建一個(gè)TreeNode類用于存儲(chǔ)我們的數(shù)據(jù)
publicclassTreeNode {
staticletNODE_TYPE_G: Int =0//表示該節(jié)點(diǎn)不是葉子節(jié)點(diǎn)
staticletNODE_TYPE_N: Int =1//表示節(jié)點(diǎn)為葉子節(jié)點(diǎn)
vartype: Int?
vardesc: String?//對(duì)于多種類型的內(nèi)容,需要確定其內(nèi)容
varid: String?
varpId: String?
varname: String?
varlevel: Int?
varisExpand: Bool = false
varicon: String?
varchildren: [TreeNode] = []
varparent: TreeNode?
init(desc: String?, id:String? , pId: String? , name: String?) {
self.desc= desc
self.id= id
self.pId= pId
self.name= name
}
//是否為根節(jié)點(diǎn)
funcisRoot() -> Bool{
returnparent == nil
}
//判斷父節(jié)點(diǎn)是否打開
funcisParentExpand() -> Bool {
ifparent == nil {
returnfalse
}
return(parent?.isExpand)!
}
//是否是葉子節(jié)點(diǎn)
funcisLeaf() -> Bool {
returnchildren.count==0
}
//獲取level,用于設(shè)置節(jié)點(diǎn)內(nèi)容偏左的距離
funcgetLevel() -> Int {
returnparent == nil ?0: (parent?.getLevel())!+1
}
//設(shè)置展開
funcsetExpand(isExpand: Bool) {
self.isExpand= isExpand
if!isExpand {
for(vari=0;i
children[i].setExpand(isExpand)
}
}
}
}
這里需要講解一下,id和pId分別對(duì)于當(dāng)前Node的ID標(biāo)示和其父節(jié)點(diǎn)ID標(biāo)示。節(jié)點(diǎn)直接建立關(guān)系它們是很關(guān)鍵的屬性。children是一個(gè)TreeNode的數(shù)組,用來(lái)存放當(dāng)前節(jié)點(diǎn)的直接子節(jié)點(diǎn)。通過children和parent兩個(gè)屬性,就可以很快的找到當(dāng)前節(jié)點(diǎn)的關(guān)系節(jié)點(diǎn)。
為了能夠操作我們的TreeNode數(shù)據(jù),我還創(chuàng)建了一個(gè)TreeNodeHelper類。
classTreeNodeHelper {
//單例模式
classvarsharedInstance: TreeNodeHelper {
structStatic {
staticvarinstance: TreeNodeHelper?
staticvartoken: dispatch_once_t =0
}
dispatch_once(&Static.token) {//該函數(shù)意味著代碼僅會(huì)被運(yùn)行一次,而且此運(yùn)行是線程同步
Static.instance= TreeNodeHelper()
}
returnStatic.instance!
}
TreeNodeHelper是一個(gè)單例模式的工具類。通過TreeNodeHelper.sharedInstance就能獲取類實(shí)例
//傳入普通節(jié)點(diǎn),轉(zhuǎn)換成排序后的Node
funcgetSortedNodes(groups: NSMutableArray, defaultExpandLevel: Int) -> [TreeNode] {
varresult: [TreeNode] = []
varnodes = convetData2Node(groups)
varrootNodes = getRootNodes(nodes)
foriteminrootNodes{
addNode(&result, node: item, defaultExpandLeval: defaultExpandLevel, currentLevel:1)
}
returnresult
}
getSortedNodes是TreeNode的入口方法。調(diào)用該方法的時(shí)候需要傳入一個(gè)Array類型的數(shù)據(jù)集。這個(gè)數(shù)據(jù)集可以是任何你想用來(lái)構(gòu)建樹形結(jié)構(gòu)的內(nèi)容。在這里我雖然只傳入了一個(gè)groups參數(shù),但其實(shí)可以根據(jù)需要重構(gòu)這個(gè)方法,傳入多個(gè)類似groups的參數(shù)。例如,當(dāng)我們需要做企業(yè)通訊錄的時(shí)候,企業(yè)通訊錄的數(shù)據(jù)中存在部門集合和用戶集合。部門之間有層級(jí)關(guān)系,用戶又屬于某個(gè)部門。我們可以將部門和用戶都轉(zhuǎn)換成TreeNode元數(shù)據(jù)。這樣修改方法可以修改為:
func getSortedNodes(groups: NSMutableArray, users: NSMutableArray, defaultExpandLevel: Int) -> [TreeNode]
是不是感覺很有意思呢?
//過濾出所有可見節(jié)點(diǎn)
funcfilterVisibleNode(nodes: [TreeNode]) -> [TreeNode] {
varresult: [TreeNode] = []
foriteminnodes {
ifitem.isRoot() || item.isParentExpand() {
setNodeIcon(item)
result.append(item)
}
}
returnresult
}
//將數(shù)據(jù)轉(zhuǎn)換成書節(jié)點(diǎn)
funcconvetData2Node(groups: NSMutableArray) -> [TreeNode] {
varnodes: [TreeNode] = []
varnode: TreeNode
vardesc: String?
varid: String?
varpId: String?
varlabel: String?
vartype: Int?
foritemingroups {
desc = item["description"]as? String
id = item["id"]as? String
pId = item["pid"]as? String
label = item["name"]as? String
node = TreeNode(desc: desc, id: id, pId: pId, name: label)
nodes.append(node)
}
/**
*設(shè)置Node間,父子關(guān)系;讓每?jī)蓚€(gè)節(jié)點(diǎn)都比較一次,即可設(shè)置其中的關(guān)系
*/
varn: TreeNode
varm: TreeNode
for(vari=0; i
n = nodes[i]
for(varj=i+1; j
m = nodes[j]
ifm.pId== n.id{
n.children.append(m)
m.parent= n
}elseifn.pId== m.id{
m.children.append(n)
n.parent= m
}
}
}
foriteminnodes {
setNodeIcon(item)
}
returnnodes
}
convetData2Node方法將數(shù)據(jù)轉(zhuǎn)換成TreeNode,同時(shí)也構(gòu)建了TreeNode之間的關(guān)系。
//獲取根節(jié)點(diǎn)集
funcgetRootNodes(nodes: [TreeNode]) -> [TreeNode] {
varroot: [TreeNode] = []
foriteminnodes {
ifitem.isRoot() {
root.append(item)
}
}
returnroot
}
//把一個(gè)節(jié)點(diǎn)的所有子節(jié)點(diǎn)都掛上去
funcaddNode(inoutnodes: [TreeNode], node: TreeNode, defaultExpandLeval: Int, currentLevel: Int) {
nodes.append(node)
ifdefaultExpandLeval >= currentLevel {
node.setExpand(true)
}
ifnode.isLeaf() {
return
}
for(vari=0; i
addNode(&nodes, node: node.children[i], defaultExpandLeval: defaultExpandLeval, currentLevel: currentLevel+1)
}
}
//設(shè)置節(jié)點(diǎn)圖標(biāo)
funcsetNodeIcon(node: TreeNode) {
ifnode.children.count>0{
node.type= TreeNode.NODE_TYPE_G
ifnode.isExpand{
//設(shè)置icon為向下的箭頭
node.icon="tree_ex.png"
}elseif!node.isExpand{
//設(shè)置icon為向右的箭頭
node.icon="tree_ec.png"
}
}else{
node.type= TreeNode.NODE_TYPE_N
}
}
}
剩下的代碼難度不大,很容易理解。需要多說一句的TreeNode.NODE\_TYPE\_G和TreeNode.NODE\_TYPE\_N是用來(lái)告訴TreeNode當(dāng)前的節(jié)點(diǎn)的類型。正如上面提到的企業(yè)通訊錄,這個(gè)兩個(gè)type就可以用來(lái)區(qū)分node數(shù)據(jù)。
TreeTableView我的重頭戲來(lái)了。它繼承了UITableView,UITableViewDataSource,UITableViewDelegate。
functableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
//通過nib自定義tableviewcell
letnib = UINib(nibName:"TreeNodeTableViewCell", bundle: nil)
tableView.registerNib(nib, forCellReuseIdentifier: NODE_CELL_ID)
varcell = tableView.dequeueReusableCellWithIdentifier(NODE_CELL_ID)as! TreeNodeTableViewCell
varnode: TreeNode = mNodes![indexPath.row]
//cell縮進(jìn)
cell.background.bounds.origin.x = -20.0* CGFloat(node.getLevel())
//代碼修改nodeIMG---UIImageView的顯示模式.
ifnode.type== TreeNode.NODE_TYPE_G{
cell.nodeIMG.contentMode= UIViewContentMode.Center
cell.nodeIMG.image= UIImage(named: node.icon!)
}else{
cell.nodeIMG.image= nil
}
cell.nodeName.text= node.name
cell.nodeDesc.text= node.desc
returncell
}
tableView:cellForRowAtIndexPath方法中,我們使用了UINib,因?yàn)槲彝ㄟ^自定義TableViewCell,來(lái)填充tableview。這里也使用了cell的復(fù)用機(jī)制。
下面我們來(lái)看控制樹形結(jié)構(gòu)展開的關(guān)鍵代碼
func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
var parentNode = mNodes![indexPath.row]
var startPosition = indexPath.row+1
var endPosition = startPosition
if parentNode.isLeaf() {//點(diǎn)擊的節(jié)點(diǎn)為葉子節(jié)點(diǎn)
// do something
} else {
expandOrCollapse(&endPosition, node: parentNode)
mNodes = TreeNodeHelper.sharedInstance.filterVisibleNode(mAllNodes!) //更新可見節(jié)點(diǎn)
//修正indexpath
var indexPathArray :[NSIndexPath] = []
var tempIndexPath: NSIndexPath?
for (var i = startPosition; i < endPosition ; i++) {
tempIndexPath = NSIndexPath(forRow: i, inSection: 0)
indexPathArray.append(tempIndexPath!)
}
//插入和刪除節(jié)點(diǎn)的動(dòng)畫
if parentNode.isExpand {
self.insertRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
} else {
self.deleteRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
}
//更新被選組節(jié)點(diǎn)
self.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)
}
}
//展開或者關(guān)閉某個(gè)節(jié)點(diǎn)
func expandOrCollapse(inout count: Int, node: TreeNode) {
if node.isExpand { //如果當(dāng)前節(jié)點(diǎn)是開著的,需要關(guān)閉節(jié)點(diǎn)下的所有子節(jié)點(diǎn)
closedChildNode(&count,node: node)
} else { //如果節(jié)點(diǎn)是關(guān)著的,打開當(dāng)前節(jié)點(diǎn)即可
count += node.children.count
node.setExpand(true)
}
}
//關(guān)閉某個(gè)節(jié)點(diǎn)和該節(jié)點(diǎn)的所有子節(jié)點(diǎn)
func closedChildNode(inout count:Int, node: TreeNode) {
if node.isLeaf() {
return
}
if node.isExpand {
node.isExpand = false
for item in node.children { //關(guān)閉子節(jié)點(diǎn)
count++ //計(jì)算子節(jié)點(diǎn)數(shù)加一
closedChildNode(&count, node: item)
}
}
}
我們點(diǎn)擊某一個(gè)非葉子節(jié)點(diǎn)的時(shí)候,將該節(jié)點(diǎn)的子節(jié)點(diǎn)添加到我們的tableView中,并給它們加上動(dòng)畫。這就是我們需要的樹形展開視圖。首先我們要計(jì)算出該節(jié)點(diǎn)的子節(jié)點(diǎn)數(shù)(在關(guān)閉節(jié)點(diǎn)的時(shí)候,還需要計(jì)算對(duì)應(yīng)的子節(jié)點(diǎn)的子節(jié)點(diǎn)的展開節(jié)點(diǎn)數(shù)),然后獲取這些子節(jié)點(diǎn)的集合,通過tableview的insertRowsAtIndexPaths和deleteRowsAtIndexPaths方法進(jìn)行插入節(jié)點(diǎn)和刪除節(jié)點(diǎn)。
tableview:didSelectRowAtIndexPath還算好理解,關(guān)鍵是expandOrCollapse和closedChildNode方法。
expandOrCollapse的作用是打開或者關(guān)閉點(diǎn)擊節(jié)點(diǎn)。當(dāng)操作為打開一個(gè)節(jié)點(diǎn)的時(shí)候,只需要設(shè)置該節(jié)點(diǎn)為展開,并且計(jì)算其子節(jié)點(diǎn)數(shù)就可以。而關(guān)閉一個(gè)節(jié)點(diǎn)就相對(duì)麻煩。因?yàn)槲覀円?jì)算子節(jié)點(diǎn)是否是打開的,如果子節(jié)點(diǎn)是打開的,那么子節(jié)點(diǎn)的子節(jié)點(diǎn)的數(shù)也要計(jì)算進(jìn)去??赡苓@里聽起來(lái)有點(diǎn)繞口,建議運(yùn)行程序后看著實(shí)例進(jìn)行理解。
3、鳴謝
借鑒的資料有:
*[swift可展開可收縮的表視圖](http://www.itdecent.cn/p/706dcc4ccb2f)
有興趣的朋友也可以參考以上兩篇blog。
License
All source code is licensed under the MIT License.