Swift開發TreeTableView

jopen 9年前發布 | 12K 次閱讀 Android開發 移動開發 TreeTableView

 

TreeTableViewWithSwift是用Swift編寫的樹形結構顯示的TableView控件。

TreeTableViewWithSwift 的由來

在開發企業通訊錄的時候需要層級展示。之前開發Android的時候有做過類似的功能,也是通過一些開源的內容進行改造利用。此次,在做ios的 同類產品時,調研發現樹形結構的控件并不是很多,雖然也有但大多看起來都比較負責,而且都是用OC編寫的。介于我的項目是Swift開發的,并且 TreeTableView貌似沒有人用Swift編寫過(也可能是我沒找到)。所以打算自己動手寫一個,從而豐衣足食。

TreeTableViewWithSwift 簡介

開發環境:Swift 2.0,XCode版本:7.0.1,ios 9.0

代碼: GitHub代碼

1、運行效果

Swift開發TreeTableView

2、關鍵代碼的解讀

TreeTableViewWithSwift其實是對tableview的擴展。在此之前需要先創建一個TreeNode類用于存儲我們的數據

publicclassTreeNode {

staticletNODE_TYPE_G: Int =0//表示該節點不是葉子節點

staticletNODE_TYPE_N: Int =1//表示節點為葉子節點

vartype: Int?

vardesc: String?//對于多種類型的內容,需要確定其內容

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

}

//是否為根節點

funcisRoot() -> Bool{

returnparent == nil

}

//判斷父節點是否打開

funcisParentExpand() -> Bool {

ifparent == nil {

returnfalse

}

return(parent?.isExpand)!

}

//是否是葉子節點

funcisLeaf() -> Bool {

returnchildren.count==0

}

//獲取level,用于設置節點內容偏左的距離

funcgetLevel() -> Int {

returnparent == nil ?0: (parent?.getLevel())!+1

}

//設置展開

funcsetExpand(isExpand: Bool) {

self.isExpand= isExpand

if!isExpand {

for(vari=0;i

children[i].setExpand(isExpand)

}

}

}

}

這里需要講解一下,id和pId分別對于當前Node的ID標示和其父節點ID標示。節點直接建立關系它們是很關鍵的屬性。children是一 個TreeNode的數組,用來存放當前節點的直接子節點。通過children和parent兩個屬性,就可以很快的找到當前節點的關系節點。

為了能夠操作我們的TreeNode數據,我還創建了一個TreeNodeHelper類。

classTreeNodeHelper {

//單例模式

classvarsharedInstance: TreeNodeHelper {

structStatic {

staticvarinstance: TreeNodeHelper?

staticvartoken: dispatch_once_t =0

}

dispatch_once(&Static.token) {//該函數意味著代碼僅會被運行一次,而且此運行是線程同步

Static.instance= TreeNodeHelper()

}

returnStatic.instance!

}

TreeNodeHelper是一個單例模式的工具類。通過TreeNodeHelper.sharedInstance就能獲取類實例

//傳入普通節點,轉換成排序后的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的入口方法。調用該方法的時候需要傳入一個Array類型的數據集。這個數據集可以是任何你想用 來構建樹形結構的內容。在這里我雖然只傳入了一個groups參數,但其實可以根據需要重構這個方法,傳入多個類似groups的參數。例如,當我們需要 做企業通訊錄的時候,企業通訊錄的數據中存在部門集合和用戶集合。部門之間有層級關系,用戶又屬于某個部門。我們可以將部門和用戶都轉換成 TreeNode元數據。這樣修改方法可以修改為:

func getSortedNodes(groups: NSMutableArray, users: NSMutableArray, defaultExpandLevel: Int) -> [TreeNode]

是不是感覺很有意思呢?

//過濾出所有可見節點

funcfilterVisibleNode(nodes: [TreeNode]) -> [TreeNode] {

varresult: [TreeNode] = []

foriteminnodes {

ifitem.isRoot() || item.isParentExpand() {

setNodeIcon(item)

result.append(item)

}

}

returnresult

}

//將數據轉換成書節點

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)

}

/**

*設置Node間,父子關系;讓每兩個節點都比較一次,即可設置其中的關系

*/

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方法將數據轉換成TreeNode,同時也構建了TreeNode之間的關系。

//獲取根節點集

funcgetRootNodes(nodes: [TreeNode]) -> [TreeNode] {

varroot: [TreeNode] = []

foriteminnodes {

ifitem.isRoot() {

root.append(item)

}

}

returnroot

}

//把一個節點的所有子節點都掛上去

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)

}

}

//設置節點圖標

funcsetNodeIcon(node: TreeNode) {

ifnode.children.count>0{

node.type= TreeNode.NODE_TYPE_G

ifnode.isExpand{

//設置icon為向下的箭頭

node.icon="tree_ex.png"

}elseif!node.isExpand{

//設置icon為向右的箭頭

node.icon="tree_ec.png"

}

}else{

node.type= TreeNode.NODE_TYPE_N

}

}

}

剩下的代碼難度不大,很容易理解。需要多說一句的TreeNode.NODE\_TYPE\_G和TreeNode.NODE\_TYPE\_N是用來告訴TreeNode當前的節點的類型。正如上面提到的企業通訊錄,這個兩個type就可以用來區分node數據。

TreeTableView我的重頭戲來了。它繼承了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縮進

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,因為我通過自定義TableViewCell,來填充tableview。這里也使用了cell的復用機制。

下面我們來看控制樹形結構展開的關鍵代碼

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {

var parentNode = mNodes![indexPath.row]

var startPosition = indexPath.row+1

var endPosition = startPosition

if parentNode.isLeaf() {//點擊的節點為葉子節點

// do something

} else {

expandOrCollapse(&endPosition, node: parentNode)

mNodes = TreeNodeHelper.sharedInstance.filterVisibleNode(mAllNodes!) //更新可見節點

//修正indexpath

var indexPathArray :[NSIndexPath] = []

var tempIndexPath: NSIndexPath?

for (var i = startPosition; i < endPosition ; i++) {

tempIndexPath = NSIndexPath(forRow: i, inSection: 0)

indexPathArray.append(tempIndexPath!)

}

//插入和刪除節點的動畫

if parentNode.isExpand {

self.insertRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

} else {

self.deleteRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

}

//更新被選組節點

self.reloadRowsAtIndexPaths([indexPath], withRowAnimation: UITableViewRowAnimation.None)

}

}

//展開或者關閉某個節點

func expandOrCollapse(inout count: Int, node: TreeNode) {

if node.isExpand { //如果當前節點是開著的,需要關閉節點下的所有子節點

closedChildNode(&count,node: node)

} else { //如果節點是關著的,打開當前節點即可

count += node.children.count

node.setExpand(true)

}

}

//關閉某個節點和該節點的所有子節點

func closedChildNode(inout count:Int, node: TreeNode) {

if node.isLeaf() {

return

}

if node.isExpand {

node.isExpand = false

for item in node.children { //關閉子節點

count++ //計算子節點數加一

closedChildNode(&count, node: item)

}

}

}

我們點擊某一個非葉子節點的時候,將該節點的子節點添加到我們的tableView中,并給它們加上動畫。這就是我們需要的樹形展開視圖。首先我 們要計算出該節點的子節點數(在關閉節點的時候,還需要計算對應的子節點的子節點的展開節點數),然后獲取這些子節點的集合,通過tableview的 insertRowsAtIndexPaths和deleteRowsAtIndexPaths方法進行插入節點和刪除節點。

tableview:didSelectRowAtIndexPath還算好理解,關鍵是expandOrCollapse和closedChildNode方法。

expandOrCollapse的作用是打開或者關閉點擊節點。當操作為打開一個節點的時候,只需要設置該節點為展開,并且計算其子節點數就可 以。而關閉一個節點就相對麻煩。因為我們要計算子節點是否是打開的,如果子節點是打開的,那么子節點的子節點的數也要計算進去。可能這里聽起來有點繞口, 建議運行程序后看著實例進行理解。

3、鳴謝

借鑒的資料有:

* [swift可展開可收縮的表視圖](http://www.jianshu.com/p/706dcc4ccb2f)

* [Android打造任意層級樹形控件考驗你的數據結構和設計](http://blog.csdn.net/lmj623565791/article/details/40212367)

有興趣的朋友也可以參考以上兩篇blog。

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!