寫在前面
好久沒有寫文章記錄總結(jié)平時(shí)用到的知識(shí)了,最近都在準(zhǔn)備新項(xiàng)目的,項(xiàng)目上手才發(fā)現(xiàn),自己可能真的是該再多努力一些了,今天記錄的這個(gè)是項(xiàng)目里面用到的一個(gè)控件,我之前有在網(wǎng)上找過(guò),看看有沒有類似的demo,然而,哈哈哈,并沒有找到合適的,大體效果就是美團(tuán) APP 上那種,如圖:
我好像就在美團(tuán)上看見了這種類似的效果,然后公司項(xiàng)目要用到類似控件,于是就自己寫了一個(gè)放在這里吧,嘿嘿
步入正題
接下來(lái)不如正題,控件效果是這樣的-----------> 圖:

大體效果大致就是像 GIF 圖上顯示的這樣,這只是個(gè)簡(jiǎn)單的大體 demo,具體的細(xì)化還沒有做呢,里面的數(shù)據(jù)也是我隨意填的,??。
大體思路
這個(gè)控件的功能也很明顯了,分左中右這三塊,大概說(shuō)一下這個(gè)控件的大體思路,從左往右,最左邊是 為了增加點(diǎn)兒視覺效果,而且選項(xiàng)不多,所以用的圖片多點(diǎn)兒,我是用的 collectionView , 里面的 cell 就特別簡(jiǎn)單了,就是一個(gè) ImageView+label 搞定的。中間需要復(fù)選,這個(gè)是用了兩個(gè)tableView,將兩個(gè)tableView進(jìn)行關(guān)聯(lián)。最右邊是一個(gè)簡(jiǎn)單的tableView 。這兩塊里用的 tableView 里面的 cell 就是用的系統(tǒng)自帶的 cell 樣式。這個(gè)控件我就是根據(jù) tableView的思想來(lái) code 的,外部方法的調(diào)用也都是通過(guò)dataSource和delegate來(lái)調(diào)用的。大體介紹完畢,接下來(lái)就結(jié)合代碼介紹一下。
代碼
第一部分:提前工作準(zhǔn)備
//MARK:- 全局變量
public let kTableViewCellHeight: Int = 43
public let kCollectionViewCellHeight: Int = 100
public let kCollectionViewHeight: Int = 220
public let kTableViewHeight: Int = 300
public let kButtomImageViewHeight: Int = 21
public let kTextColor: UIColor = UIColor(red: 51/255.0, green: 51/255.0, blue: 51/255.0, alpha: 1)
public let kDetailTextColor: UIColor = UIColor(red: 136/255.0, green: 136/255.0, blue: 136/255.0, alpha: 1)
public let kSepatatorColor: UIColor = UIColor(red: 219/255.0, green: 219/255.0, blue: 219/255.0, alpha: 1)
public let kCellBgColor: UIColor = UIColor(red: 245/255.0, green: 245/255.0, blue: 245/255.0, alpha: 1)
public let kTextSelectColor: UIColor = UIColor(red: 253/255.0, green: 191/255.0, blue: 44/255.0, alpha: 1)```
這個(gè)沒有什么可以介紹的,我就是比較喜歡把可能用到的變量寫在前面,便于修改啊什么的。
######第二部分:DOPIndexPath
//MARK:DOPIndexPath
class DOPIndexPath: NSObject {
var column:Int?
var row:Int?
var item:Int?
init(dopColumn: Int, dopRow: Int) {
column = dopColumn
row = dopRow
item = -1
}
convenience init(dopColumn: Int, dopRow: Int, dopItem: Int) {
self.init(dopColumn: dopColumn, dopRow: dopRow)
item = dopItem
}
static func indexPathWith(col: Int, row: Int) -> DOPIndexPath {
let indexPath = DOPIndexPath.init(dopColumn: col, dopRow: row)
return indexPath
}
static func indexPathWith(col: Int, row: Int, item: Int) -> DOPIndexPath {
return DOPIndexPath(dopColumn: col, dopRow: row, dopItem: item)
}
}
這個(gè)就是根據(jù)```tableView```的思路,聲明了一個(gè)``` IndexPath ```類,這里的 ```column```是用來(lái)區(qū)分 titleBar 上是第幾列的

```row``` 就是控件中的```tableView```中```cell```的行數(shù),```item```是中間雙```tableView```中右邊```rightTableView```中```cell```用的。
看著這些方法是不是很熟悉啊,就是 ```tableView ``` 中我們經(jīng)常用到的方法。??
######DOPDropDownMenuDataSource+DOPDropDownMenuDelegate
//MARK:DOPDropDownMenuDataSource
@objc protocol DOPDropDownMenuDataSource : NSObjectProtocol {
//返回 menu 第column列有多少行
func menu(dopMenu menu: DOPDropDownMenu, numberOfRowsInColumn column: Int) -> Int
//返回 menu 第column列 每行title
func menu(dopMenu menu: DOPDropDownMenu, titleForRowAtIndexPath indexPath: DOPIndexPath) -> String
//返回 menu 有多少列, 默認(rèn)1列
func numberOfColumnsInMenu(dopMenu menu: DOPDropDownMenu) -> Int
//新增 返回 menu 第column列 每行image
@objc optional func menu(dopMenu menu: DOPDropDownMenu, imageNameForRowAtIndexPath indexPath: DOPIndexPath) -> String
//新增 detailText, right text
@objc optional func menu(dopMenu menu: DOPDropDownMenu, detailTextForRowAtIndexPath indexPath: DOPIndexPath) -> String
//新增 當(dāng)有column列 row行 返回有多少個(gè)item, 如果>0, 說(shuō)明有二級(jí)列表, =0 沒有二級(jí)列表
@objc optional func menu(dopMenu menu: DOPDropDownMenu, numberOfItemsInRow row:Int, column: Int) -> Int
//新增 當(dāng)有column列 row行 item項(xiàng) title 如果都沒有可以不實(shí)現(xiàn)該協(xié)議
@objc optional func menu(dopMenu menu: DOPDropDownMenu, titleForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String
//新增 當(dāng)有column列 row行 item項(xiàng) image
@objc optional func menu(dopMenu menu: DOPDropDownMenu, imageNameForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String
//新增 當(dāng)有column列 row行 item項(xiàng) title
@objc optional func menu(dopMenu menu: DOPDropDownMenu, detailTextForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String
}
//MARK:-DOPDropDownMenuDelegate
@objc protocol DOPDropDownMenuDelegate : NSObjectProtocol {
//點(diǎn)擊代理, 點(diǎn)擊了第column 第row 或者item項(xiàng), 如果item >= 0
@objc optional func menu(_ menu: DOPDropDownMenu, didSelectRowAtIndexPath indexPath: DOPIndexPath) -> Void
//
@objc optional func menu(_ menu: DOPDropDownMenu, willSelectRowAtIndexPath indexPath: DOPIndexPath) -> IndexPath
}
這一部分是聲明的 protocol, 便于在外部調(diào)用,每個(gè)方法都有注釋了,里面的方法和```tableView ```也是很相似。
######DOPBackgroundCellView
//MARK:DOPBackgroundCellView
class DOPBackgroundCellView:UIView {
override func draw(_ rect: CGRect) {
//Drawing code
let context: CGContext = UIGraphicsGetCurrentContext()!
//畫一條底部線
context.setStrokeColor(red: 219.0/255, green: 224.0/255, blue: 228.0/255, alpha: 1);//線條顏色
context.move(to: CGPoint(x: 0, y: 0))
context.addLine(to: CGPoint(x: rect.size.width, y: 0))
context.move(to: CGPoint(x: 0, y: rect.size.height))
context.addLine(to: CGPoint(x: rect.size.width, y: rect.size.height))
context.strokePath()
}
}
這個(gè)是用 context 畫的一個(gè) cell 選中的背景 view,當(dāng)然也可以用別的代替啊
######DataSourceFlags
struct dataSourceFlags {
var numberOfRowsInColumn : Int = 1
var numberOfItemsInRow : Int = 1
var titleForRowAtIndexPath : Int = 1
var titleForItemsInRowAtIndexPath : Int = 1
var imageNameForRowAtIndexPath : Int = 1
var imageNameForItemsInRowAtIndexPath : Int = 1
var detailTextForRowAtIndexPath : Int = 1
var detailTextForItemsInRowAtIndexPath : Int = 1
}
這個(gè)是聲明的一個(gè)結(jié)構(gòu)體,里面存放的是每個(gè) protocol 返回的 Num,以便在本體里進(jìn)行判斷操作。名字和 protocol 的方法名相同。
######DOPDropDownMenu
//MARK:DOPDropDownMenu
這一部分代碼比較多,先說(shuō)控件上所有的 title,image 都是用自定義的動(dòng)畫實(shí)現(xiàn)的。主要有以下幾個(gè)方法:
fileprivate func animateIndicator(indicator: CAShapeLayer, forward:Bool, complete:()->()) -> Void
animateBackGroundView
fileprivate func animateBackGroundView(view: UIView, show: Bool, complete:()->()) -> Void
fileprivate func animateCollectionView(collectionView: UICollectionView, show: Bool, complete:()->()) -> Void
fileprivate func animateTableView(tableView: UITableView?, show: Bool, complete:()->()) -> Void
fileprivate func animateTitle(title: CATextLayer, show: Bool, complete: ()->()) -> Void
fileprivate func animateIdicator(indicator: CAShapeLayer, background:UIView, tableview: UITableView, title: CATextLayer, forward: Bool, complete:()->()) -> Void
fileprivate func animateCollectionIdicator(indicator: CAShapeLayer, background:UIView, collectionView: UICollectionView, title: CATextLayer, forward: Bool, complete:()->()) -> Void
方法中包含了```title```,```indicator```,```BackGroundView```,```tableVIew```,```collectionView```改變效果的實(shí)現(xiàn).動(dòng)畫中的方法基本都是關(guān)聯(lián)的,我將它們拆分開,以便復(fù)用,每個(gè)方法中也比較多的frame 變換計(jì)算。
在前面說(shuō)過(guò)了,這個(gè)控件是由```collectionView```和```tableView```構(gòu)成的, 在init 方法中初始化```collectionView```、```tableView```、```bottomImageView ```(底部的條)、``` navBar``` 的``` tapGesture```(手勢(shì)),```backgroundTapGesture```(點(diǎn)擊背景消失手勢(shì))、、、
在 ```dataSource ```的 set 方法中給結(jié)構(gòu)體中的```structDataSourceFlags```各項(xiàng)復(fù)0和1值,便于接下來(lái)的往外傳代理方法時(shí)候進(jìn)行判斷。
structDataSourceFlags.numberOfRowsInColumn = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:numberOfRowsInColumn:))))! ? 1 : 0
structDataSourceFlags.numberOfItemsInRow = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:numberOfItemsInRow:column:))))! ? 1 : 0
structDataSourceFlags.titleForRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:titleForRowAtIndexPath:))))! ? 1 : 0
structDataSourceFlags.titleForItemsInRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:titleForItemsInRowAtIndexPath:))))! ? 1 : 0
structDataSourceFlags.imageNameForRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:imageNameForRowAtIndexPath:))))! ? 1 : 0
structDataSourceFlags.imageNameForItemsInRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:imageNameForItemsInRowAtIndexPath:))))! ? 1 : 0
structDataSourceFlags.detailTextForRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:detailTextForRowAtIndexPath:))))! ? 1 : 0
structDataSourceFlags.detailTextForItemsInRowAtIndexPath = (self.dataSource?.responds(to: #selector(DOPDropDownMenuDataSource.menu(dopMenu:detailTextForItemsInRowAtIndexPath:))))! ? 1 : 0
然后就是 ```nav```的構(gòu)建。包括:title,separator,Indicators 表現(xiàn)在控件上就是,navBar 上的題目,分割線,和上下的指示條(這里指示條有 bug,我還沒有找出來(lái),想著后期用 ImgView 替代)根據(jù)傳入的 navBar 上 title 的數(shù)目和 column 數(shù)量進(jìn)行構(gòu)建 navBar,并將 title,separator,Indicators放進(jìn)數(shù)組里面儲(chǔ)存,以便進(jìn)行操作(比如點(diǎn)擊某個(gè)選項(xiàng)后 title 改變)。
接下來(lái)說(shuō)的就是 navBar 上的點(diǎn)擊事件的響應(yīng)了,由于最左邊的是```collectionView``` 和其余兩個(gè)不一樣(其余兩個(gè)是 tableView)所以在處理點(diǎn)擊方法時(shí)候需要判斷是不是 column 的 tapIndex == 0? 和 所選的 view 是否是打開的 isShow? ,一共四種大情況,還需要再判讀 collectionView的狀態(tài)進(jìn)行 show OR close。
if tapIndex == 0 && isShow {
if (self.superview?.subviews.contains(self.leftTableView))! || (self.superview?.subviews.contains(self.rightTableView))! {
self.animateIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, tableview: self.leftTableView, title: titles[currentSelectedMenudIndex], forward: false, complete: {
isShow = false
})
} else {
}
self.animateCollectionIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, collectionView: self.collectionView!, title: titles[currentSelectedMenudIndex], forward: false, complete: {
currentSelectedMenudIndex = tapIndex
isShow = false
})
} else if tapIndex == 0 && !isShow {
currentSelectedMenudIndex = tapIndex
// self.collectionView?.reloadData()
self.animateCollectionIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, collectionView: self.collectionView!, title: titles[currentSelectedMenudIndex], forward: true, complete: {
currentSelectedMenudIndex = tapIndex
isShow = true
})
}
if isShow && tapIndex != 0{
if (self.superview?.subviews.contains(self.collectionView!))! {
self.animateCollectionIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, collectionView: self.collectionView!, title: titles[currentSelectedMenudIndex], forward: false, complete: {
currentSelectedMenudIndex = tapIndex
isShow = false
})
} else {
}
self.animateIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, tableview: self.leftTableView, title: titles[currentSelectedMenudIndex], forward: false, complete: {
currentSelectedMenudIndex = tapIndex
isShow = false
})
} else if tapIndex != 0 && !isShow{
currentSelectedMenudIndex = tapIndex
self.leftTableView.reloadData()
if (dataSource != nil) && structDataSourceFlags.numberOfItemsInRow != 0 {
// self.rightTableView.reloadData()
}
self.animateIdicator(indicator: indicators[tapIndex], background: backGroundView, tableview: self.leftTableView, title: titles[tapIndex], forward: true, complete: {
isShow = true
})
}
處理完 navBar 上的點(diǎn)擊事件然后就是 ```backGroundView``` 的點(diǎn)擊事件方法了:
@objc fileprivate func backGroundTapped(paramSender: UITapGestureRecognizer) -> Void {
if (self.superview?.subviews.contains(collectionView!))! {
self.animateCollectionIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, collectionView: self.collectionView!, title: titles[currentSelectedMenudIndex], forward: false, complete: {
isShow = false
})
} else {
self.animateIdicator(indicator: indicators[currentSelectedMenudIndex], background: backGroundView, tableview: self.leftTableView, title: titles[currentSelectedMenudIndex], forward: false) {
isShow = false
}
}
}
然后就是 ```tableView```和```collectionView```的代理方法了,這里的 ```tableView```用了 兩個(gè),用來(lái)區(qū)分顯示一個(gè)還是顯示兩個(gè)就是用到 item 是否有數(shù)據(jù)來(lái)判斷。
######使用
dopMenu = DOPDropDownMenu(origin: CGPoint(x: 0, y: 64), height: 44)
dopMenu.dataSource = self
dopMenu.delegate = self
self.view.addSubview(dopMenu)
控件的初始化就特別簡(jiǎn)單了,設(shè)置好```frame``` 聲明```delegate```和```dataSource```是self,就好了
控件中賦值和響應(yīng)方法是通過(guò)代理方法調(diào)用的:
delegate:
//MARK:- delegate
func numberOfColumnsInMenu(dopMenu menu: DOPDropDownMenu) -> Int {
return 3
}
func menu(dopMenu menu: DOPDropDownMenu, numberOfRowsInColumn column: Int) -> Int {
if column == 0 {
return allSorts.count
} else if column == 1 {
return sifts.count
} else {
return sorts.count
}
}
func menu(dopMenu menu: DOPDropDownMenu, titleForRowAtIndexPath indexPath: DOPIndexPath) -> String {
if indexPath.column == 0 {
return allSorts[indexPath.row!]
} else if indexPath.column == 1 {
return sifts[indexPath.row!]
} else {
return sorts[indexPath.row!]
}
}
dataSource:
//new datasource
func menu(dopMenu menu: DOPDropDownMenu, imageNameForRowAtIndexPath indexPath: DOPIndexPath) -> String {
if indexPath.column == 0 || indexPath.column == 1 {
if let num = indexPath.row {
return "ic_filter_category_(num)"
}
}
return ""
}
func menu(dopMenu menu: DOPDropDownMenu, imageNameForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String {
if indexPath.column == 0 && indexPath.item! >= 0 {
if let num = indexPath.row {
return "ic_filter_category_\(num)"
}
}
return ""
}
func menu(dopMenu menu: DOPDropDownMenu, detailTextForRowAtIndexPath indexPath: DOPIndexPath) -> String {
if indexPath.column! < 3 {
return String(arc4random()/1000)
}
return "00"
}
func menu(dopMenu menu: DOPDropDownMenu, detailTextForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String {
return String(arc4random()/1000)
}
func menu(dopMenu menu: DOPDropDownMenu, numberOfItemsInRow row: Int, column: Int) -> Int {
if column == 1 {
if row == 0 {
return cates.count
} else if row == 1 {
return movices.count
} else if row == 2 {
return hotels.count
}
}
return 0
}
func menu(dopMenu menu: DOPDropDownMenu, titleForItemsInRowAtIndexPath indexPath: DOPIndexPath) -> String {
if indexPath.column == 1 {
if indexPath.row == 0 {
return cates[indexPath.item!]
} else if indexPath.row == 1 {
return movices[indexPath.item!]
} else if indexPath.row == 2 {
return hotels[indexPath.item!]
}
}
return "沒有"
}
func menu(_ menu: DOPDropDownMenu, didSelectRowAtIndexPath indexPath: DOPIndexPath) {
if indexPath.item! > 0 {
print("點(diǎn)擊了第\(indexPath.column)列 - 第\(indexPath.row)行 - 第\(indexPath.item)項(xiàng)")
} else {
print("點(diǎn)擊了第\(indexPath.column)列 - 第\(indexPath.row)行")
}
}
使用方法就和我們平時(shí)用的 tableView類似,就是根據(jù)column,row,item進(jìn)行操作
#####總結(jié)
項(xiàng)目大致流程就是這樣子,代碼有點(diǎn)兒多了,也不能具體拿出來(lái)分析了,里面各種方法穿插使用,可能會(huì)暈,具體的方法使用還是看代碼的,自己回經(jīng)?;仡^看的,這篇文章也會(huì)多多彌補(bǔ)缺漏的。。。。
代碼鏈接在這:https://github.com/irembeu/SwiftDropDownMenuDemo