iOS多級下拉菜單開發(fā)

為接下來我們要做什么有個底蚀乔,先來看看完成后的效果圖烁竭。總的來說我們要實現(xiàn)這樣一個下拉控件吉挣,點擊城市按鈕颖变,就會彈出城市選擇下拉列表,點擊第一列或者第二列就會根據(jù)點擊的行刷新后面的列表听想,比如點擊了省份這一列會更新相應(yīng)的城市列表和區(qū)縣列表腥刹,大概可以用級聯(lián)更新這個詞來描述吧!在點擊最后一列后列表會消失汉买,點擊灰色區(qū)域城市列表也會消失衔峰,在列表展示的情況下,點擊城市按鈕蛙粘,城市列表也會消失垫卤。停止嗶嗶????,擼起袖子開始干出牧!

我們這次的重點是多級下拉列表的開發(fā)穴肘,因此我們從一個初始項目開始,直接進入下拉列表的開發(fā)的開發(fā)環(huán)節(jié)????舔痕。從這里下載我們的初始項目评抚,下圖是初始項目運行后的結(jié)果。

做一個小小的說明:

  • 這次我們項目中使用SnapKit以純代碼的方式進行界面布局伯复,不熟悉的朋友不同擔心慨代,我們的Demo項目界面比較簡單,布局代碼也是簡潔明了的啸如。
  • 另外城市列表數(shù)據(jù)格式為json侍匙,我們通過Unbox第三方庫,將json數(shù)據(jù)轉(zhuǎn)成項目中可以使用的Model
  • 這兩個庫已經(jīng)通過pod安裝叮雳,包含在初始項目中想暗,初始列表還包括城市列表數(shù)據(jù)

從此刻開始我們的多級下拉控件叫做MultiLevelMenu

我們使用多個UITableView來實現(xiàn)多個列表妇汗,從完成效果圖來看,這里有3個UITableView對象说莫,可是為了把這個MultiLevelMenu控件做成一個通用控件铛纬,我們需要讓外界提供給數(shù)據(jù)給MultiLevelMenu,然后進行展示唬滑。類似UITableView的數(shù)據(jù)源方法告唆,我們也會創(chuàng)建DataSource 協(xié)議,讓外界通過這個協(xié)議為MultiLevelMenu提供數(shù)據(jù)晶密。

好開心擒悬,好開心,終于可以敲代碼了??????稻艰!

打開項目文件導(dǎo)航欄懂牧,選中View這個Group,(command + n)創(chuàng)建一個UIView的子類MultiLevelMenu尊勿,這個就是我們的多級下拉列表啦僧凤!剛剛的操作可以看一看下面的步驟截圖。

打開MultiLevelMenu.swift文件元扔,添加初始化方法躯保,在這里我們將背景色設(shè)置為黑色,透明度為0.5澎语。這樣做的目的是途事,在展示MultiLevelMenu控件時,會有一層灰黑色蒙版擅羞,具體可以參考完成后的效果圖尸变。

import UIKit

class MultiLevelMenu: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor(white: 0, alpha: 0.5)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

在類MultiLevelMenu定義的上方,添加一個數(shù)據(jù)源協(xié)議MultiLevelMenuDataSource减俏,聲明如下

@objc protocol MultiLevelMenuDataSource: NSObjectProtocol {
    @objc func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int
}

現(xiàn)在MultiLevelMenuDataSource聲明中只有一個方法召烂,外界通過這個數(shù)據(jù)源方法,告訴MultiLevelMenu要顯示多少個級別的數(shù)據(jù)娃承,比如返回3奏夫,那么就會有3個列表。MultiLevelMenu通過這個數(shù)據(jù)源方法草慧,配置列表和數(shù)據(jù)桶蛔。

數(shù)據(jù)源協(xié)議聲明好了匙头,我們在MultiLevelMenu類中添加一個dataSource屬性漫谷,代表數(shù)據(jù)源。

weak var dataSource: MultiLevelMenuDataSource?

暫時把數(shù)據(jù)源放一放蹂析,為了在table view上展示城市信息舔示,定義一個UITableViewCell的子類LevelItemCell碟婆,在MultiLevelMenuDataSource的聲明下面定義這個類。這個cell非常簡單惕稻,只包含了一個label控件竖共,設(shè)置了label的文字居中。同時為LevelItemCell增加了紫色選中效果俺祠。

fileprivate class LevelItemCell: UITableViewCell {
    
    lazy var levelNamelabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14)
        label.textAlignment = .center
        return label
    }()
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.backgroundColor = UIColor.clear
        
        // 布局label
        self.contentView.addSubview(levelNamelabel)
        levelNamelabel.snp.makeConstraints { (make) in
            make.edges.equalToSuperview().inset(UIEdgeInsetsMake(8, 12, 8, 12))
        }
        
        // 添加紫色選中效果
        let view = UIView()
        view.backgroundColor = UIColor(red: 198 / 255.0, green: 165 / 255.0, blue: 223 / 255.0, alpha: 0.3)
        self.selectedBackgroundView = view
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

好了公给,有了LevelItemCell,接著就要在table view中展示它們了蜘渣√暑恚回到MultiLevelMenu類中,在init(frame: CGRect)初始化方法的上方蔫缸,添加3個私有變量腿准,分別代表cell的重用標識符,有多少級列表需要展示拾碌,以及存儲展示的列表吐葱。

fileprivate let LevelItemCellReuseIdentifier = "LevelItemCellReuseIdentifier"
fileprivate lazy var numberOfLvel = 0
fileprivate lazy var menuTableViews = [UITableView]()

再添加一個容器view,我們的城市列表都會放在這個容器view中校翔,方便管理和實現(xiàn)一些動畫效果弟跑。在menuTableViews變量的下方,創(chuàng)建這個view

private lazy var containerView: UIView = {
    let view = UIView()
    return view
}()

接下來我們在MultiLevelMenu類中添加兩個私有方法防症,用來創(chuàng)建展示城市數(shù)據(jù)的table view窖认,以及對table view進行布局。

// 創(chuàng)建table view
private func createMenuTableView() {
    for level in 0..<self.numberOfLvel {
        let tableView = UITableView()
        tableView.register(LevelItemCell.self, forCellReuseIdentifier: LevelItemCellReuseIdentifier)
        tableView.tag = 100 + level
        tableView.rowHeight = UITableViewAutomaticDimension
        tableView.estimatedRowHeight = 40
        tableView.tableFooterView = UIView()
        tableView.showsVerticalScrollIndicator = false
        tableView.separatorColor = UIColor(white: 0.9, alpha: 1.0)
        tableView.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0)
        tableView.dataSource = self
        tableView.delegate = self
        
        menuTableViews.append(tableView)
    }
    
    configureLayout()
}

// 布局table view
private func configureLayout() {
    self.addSubview(containerView)
    for (index, tableView) in menuTableViews.enumerated() {
        containerView.addSubview(tableView)
        
        tableView.snp.makeConstraints({ (make) in
            make.top.bottom.equalToSuperview()
            make.width.equalToSuperview().multipliedBy(1.0 / Double(self.numberOfLvel))
            
            if index == 0 {
                make.leading.equalToSuperview()
            } else {
                make.leading.equalTo(self.menuTableViews[index - 1].snp.trailing)
            }
        })
    }
    
    containerView.snp.makeConstraints { (make) in
        make.leading.top.trailing.equalToSuperview()
        make.height.equalTo(240)
    }
}

創(chuàng)建table view這個方法很好懂告希,在這里對table view做了一些常規(guī)的配置扑浸,由于我們還沒有實現(xiàn)UITableView的數(shù)據(jù)源方法和delegate方法,這里編譯器會有錯誤提示燕偶,這個不要緊喝噪,我們后面加上去。

在布局table view的方法中指么,為每個table view添加約束酝惧,table view的top和bottom和containerView一致;width為containerView的1.0 / numberOfLvel伯诬,也就是說如果有3個級別晚唇,那么每個列表的寬度為containerView的1/3;table view的左邊的約束設(shè)置與containerView的左邊相等或者與前一級列表的右邊相等盗似。最后設(shè)置containerView的左邊哩陕,上邊,右邊約束與MultiLevelMenu對象本身相等,height約束為240悍及,這個可以根據(jù)需要改變闽瓢。希望我有解釋清楚這個方法????。

現(xiàn)在我們使用extension的方式為MultiLevelMenu添加UITableView的數(shù)據(jù)源方法和delegate方法心赶。在MultiLevelMenu類最后的花括號下面添加以下代碼段扣讼。

extension MultiLevelMenu: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let level = tableView.tag - 100
        let cell = tableView.dequeueReusableCell(withIdentifier: LevelItemCellReuseIdentifier, for: indexPath) as! LevelItemCell
        cell.levelNamelabel.text = "level \(level)-\(indexPath.row)"
        return cell
    }
}

extension MultiLevelMenu: UITableViewDelegate {
    
}

這里我們添加這兩段代碼純粹是為了驗證能否將MultiLevelMenu正確的展示出來。

我們?yōu)镸ultiLevelMenu的dataSource屬性添加一個屬性觀察器缨叫,當dataSource被設(shè)置之后椭符,我們就調(diào)用數(shù)據(jù)源方法numberOfLevel(of multiLevelMenu: MultiLevelMenu),獲得需要展示多少個級別的信息并創(chuàng)建table view耻姥。代碼如下

weak var dataSource: MultiLevelMenuDataSource? {
    didSet {
        guard let dataSource = dataSource else {
            return
        }
        
        numberOfLvel = dataSource.numberOfLevel(of: self)
        createMenuTableView()
    }
}

為了有個參考艰山,貼出到目前為止整個MultiLevelMenu.swift文件的代碼。

import UIKit

@objc protocol MultiLevelMenuDataSource: NSObjectProtocol {
    @objc func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int
}

fileprivate class LevelItemCell: UITableViewCell {
    
    lazy var levelNamelabel: UILabel = {
        let label = UILabel()
        label.font = UIFont.systemFont(ofSize: 14)
        label.textAlignment = .center
        return label
    }()
    
    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        self.backgroundColor = UIColor.clear
        
        // 布局label
        self.contentView.addSubview(levelNamelabel)
        levelNamelabel.snp.makeConstraints { (make) in
            make.edges.equalToSuperview().inset(UIEdgeInsetsMake(8, 12, 8, 12))
        }
        
        // 添加紫色選中效果
        let view = UIView()
        view.backgroundColor = UIColor(red: 198 / 255.0, green: 165 / 255.0, blue: 223 / 255.0, alpha: 0.3)
        self.selectedBackgroundView = view
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

class MultiLevelMenu: UIView {
    
    //MARK: - 公開變量
    weak var dataSource: MultiLevelMenuDataSource? {
        didSet {
            guard let dataSource = dataSource else {
                return
            }
            
            numberOfLvel = dataSource.numberOfLevel(of: self)
            createMenuTableView()
        }
    }
    
    //MARK: - 私有變量
    fileprivate let LevelItemCellReuseIdentifier = "LevelItemCellReuseIdentifier"
    fileprivate lazy var numberOfLvel = 0
    fileprivate lazy var menuTableViews = [UITableView]()
    
    private lazy var containerView: UIView = {
        let view = UIView()
        return view
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)
        self.backgroundColor = UIColor(white: 0, alpha: 0.5)
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // 創(chuàng)建table view
    private func createMenuTableView() {
        for level in 0..<self.numberOfLvel {
            let tableView = UITableView()
            tableView.register(LevelItemCell.self, forCellReuseIdentifier: LevelItemCellReuseIdentifier)
            tableView.tag = 100 + level
            tableView.rowHeight = UITableViewAutomaticDimension
            tableView.estimatedRowHeight = 40
            tableView.tableFooterView = UIView()
            tableView.showsVerticalScrollIndicator = false
            tableView.separatorColor = UIColor(white: 0.9, alpha: 1.0)
            tableView.separatorInset = UIEdgeInsetsMake(0, 0, 0, 0)
            tableView.dataSource = self
            tableView.delegate = self
            
            menuTableViews.append(tableView)
        }
        
        configureLayout()
    }
    
    // 布局table view
    private func configureLayout() {
        self.addSubview(containerView)
        for (index, tableView) in menuTableViews.enumerated() {
            containerView.addSubview(tableView)
            
            tableView.snp.makeConstraints({ (make) in
                make.top.bottom.equalToSuperview()
                make.width.equalToSuperview().multipliedBy(1.0 / Double(self.numberOfLvel))
                
                if index == 0 {
                    make.leading.equalToSuperview()
                } else {
                    make.leading.equalTo(self.menuTableViews[index - 1].snp.trailing)
                }
            })
        }
        
        containerView.snp.makeConstraints { (make) in
            make.leading.top.trailing.equalToSuperview()
            make.height.equalTo(240)
        }
    }
}

extension MultiLevelMenu: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let level = tableView.tag - 100
        let cell = tableView.dequeueReusableCell(withIdentifier: LevelItemCellReuseIdentifier, for: indexPath) as! LevelItemCell
        cell.levelNamelabel.text = "level \(level)-\(indexPath.row)"
        return cell
    }
}

extension MultiLevelMenu: UITableViewDelegate {
    
}

接著我們切換到HomeViewController.swift文件咏闪,對MultiLevelMenu做個簡單的展示驗證萨脑。在createSeparateLine()方法下方添加一個presentMultiLevelMunu(sender: UIButton)方法备图。

@objc private func presentMultiLevelMunu(sender: UIButton) {
    let multiLevelMenu = MultiLevelMenu(frame: CGRect(x: 0, y: 120, width: self.view.frame.width, height: 500))
    multiLevelMenu.dataSource = self
    self.view.addSubview(multiLevelMenu)
}

找到cityButton定義的地方骨宠,用下面的代碼段替換它髓窜。點擊cityButton將會展示MultiLevelMenu。

private lazy var cityButton: UIButton = {
    let button = self.createButton(title: "城市")
    button.addTarget(self, action: #selector(presentMultiLevelMunu(sender:)), for: .touchUpInside)
    return button
}()

最后實現(xiàn)MultiLevelMenu的數(shù)據(jù)源方法据某,告訴MultiLevelMenu需要展示多少級數(shù)據(jù)橡娄。

extension HomeViewController: MultiLevelMenuDataSource {
    func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int {
        return 3
    }
}

??激動人心的時刻來啦,運行一次看看效果??癣籽。點擊城市按鈕之后挽唉,可以看到我們已經(jīng)將MultiLevelMenu展示出來了????。

這里我們是直接把MultiLevelMenu添加到當前controller的view上的筷狼,接下來我們換一種方式來展示MultiLevelMenu瓶籽,在UIWindow上添加MultiLevelMenu。切換到MultiLevelMenu.swift文件埂材,添加一個變量用來記錄MultiLevelMenu是否已經(jīng)彈出塑顺,再添加兩個方法,分別用來彈出和移除MultiLevelMenu俏险。

  • func presnt(from view: UIView)严拒,從一個view的下方彈出MultiLevelMenu,比如傳進來一個button竖独,那么將在這個button的下方顯示MultiLevelMenu控件裤唠。
  • func dismiss(animated: Bool) 將MultiLevelMenu控件從UIWindow移除。
  • var isShowed = false莹痢,記錄MultiLevelMenu的顯示狀態(tài)
func presnt(from view: UIView) {
    var tmpSuperView = view
    
    // 尋找最上級view
    while tmpSuperView.superview != nil {
        tmpSuperView = tmpSuperView.superview!
    }
    
    let window = UIApplication.shared.keyWindow
    window?.addSubview(self)
    
    // 進行坐標變化种蘸,得到控件左下角相對于最上級view的坐標
    var presentPoint: CGPoint
    if view.superview == tmpSuperView {
         presentPoint = CGPoint(x: view.frame.minX, y: view.frame.maxY)
    } else {
         presentPoint = tmpSuperView.convert(CGPoint(x: view.frame.minX, y: view.frame.maxY), from: view)
    }
    
    self.snp.makeConstraints { (make) in
        make.top.equalToSuperview().offset(presentPoint.y)
        make.leading.trailing.bottom.equalToSuperview()
    }
    
    isShowed = true
}
    
func dismiss(animated: Bool) {
    self.removeFromSuperview()
    isShowed = false
}

這里有必要對func presnt(from view: UIView)解釋一下墓赴,傳進來一個view,我們需要找到它最上級的view(也就是superView)劈彪,比如下面的示例圖竣蹦,view2和view3的最上級view都是view1顶猜,這里就涉及了一個坐標系問題沧奴,view3的坐標是相對于view2,而view2的坐標是相對于view1的长窄。假如我們現(xiàn)在從view3彈出MultiLevelMenu滔吠,那么就需要將view3的坐標變換到view1的坐標系中,如果從view2彈出MultiLevelMenu挠日,則不需要進行坐標變換疮绷,因為view2的坐標本身就是相對于view1的。

我們在func presnt(from view: UIView)方法中將MultiLevelMenu添加到window上嚣潜,對MultiLevelMenu添加約束冬骚,使MultiLevelMenu顯示的位置剛好在傳進來view的下方,并占滿剩余的空間懂算。

切換到HomeViewController.swift文件只冻,讓我們來試試,剛剛添加的方法计技。

在viewDidLoad()方法的上方添加一個MultiLevelMenu的示例變量喜德。

private lazy var multiLevelMenu: MultiLevelMenu = {
    let multiLevelMenu = MultiLevelMenu()
    multiLevelMenu.dataSource = self
    return multiLevelMenu
}()

接著我們修改presentMultiLevelMunu(sender: UIButton)方法,如果MultiLevelMenu沒有彈出垮媒,我們就顯示它舍悯,如果已經(jīng)顯示,我們就移除它睡雇。

@objc private func presentMultiLevelMunu(sender: UIButton) {
    multiLevelMenu.isShowed ? multiLevelMenu.dismiss(animated: true) : multiLevelMenu.presnt(from: sender)
}

運行一遍試試吧????萌衬!我們正確的將MultiLevelMenu,顯示在cityButton的下方它抱,而且顯示奄薇,移除都沒有問題。

接下來我們需要為MultiLevelMenu抗愁,填充點真實的數(shù)據(jù)了馁蒂,這部分才是我們真正的重點,所以打起精神吧少年V╇纭沫屡!。

看看我們每一級的table view需要顯示什么數(shù)據(jù)撮珠,可以發(fā)現(xiàn)只要顯示某個level的名稱就夠了沮脖,選中一個level就會顯示所有下一級的名稱金矛,因此我們可以定義一個Level類,來代表每一個需要顯示的Level節(jié)點勺届。

class Level {
    var levelName = ""
    var netLevelItems: [Level]?
}

我們可以讓具體的Model去繼承Level類驶俊,在MultiLevelMenu的使用上只需要知道levelName和對應(yīng)的下一級就夠了,所以MultiLevelMenu能夠把Level類或者它的子類作為自己的數(shù)據(jù)源免姿。

為了踐行面向協(xié)議編程的思想饼酿,這里不使用繼承的方式實現(xiàn)MultiLevelMenu的數(shù)據(jù)源結(jié)構(gòu),而將Level類以協(xié)議的方式實現(xiàn)胚膊。在View這個group中新建一個swift文件故俐,命名為 LeveItemProtocol.swift,在這個文件中紊婉,我們定義LeveItemProtocol药版。

import Foundation

@objc protocol LeveItemProtocol: NSObjectProtocol {
    var levelName: String { get }
    var nextLevelItems: [LeveItemProtocol]? { get }
}

LeveItemProtocol定義了兩個屬性,當前級別的名稱和下一級別喻犁,如果當前級別是最后一級槽片,那么下一級就是nil,所以下一級是optional類型的肢础。

接下來我們將定義一個Model还栓,用來表示城市信息。打開Resources這個group中的china-city-info.json文件乔妈,可以看到大量的json格式的城市信息數(shù)據(jù)蝙云,這里會使用到Unbox第三方庫將JSON數(shù)據(jù)轉(zhuǎn)為模型。

在Model這個group中新建一個swift文件路召,命名為CityModel.swift勃刨,將文件內(nèi)容替換為下面的代碼段。

import Foundation
import Unbox

class CityModel: NSObject, Unboxable, LeveItemProtocol {
    
    let name: String
    let subCitys: [CityModel]?
    
    var levelName: String {
        return self.name
    }
    
    var nextLevelItems: [LeveItemProtocol]? {
        return self.subCitys
    }
    
    required init(unboxer: Unboxer) throws {
        name = try unboxer.unbox(key: "name")
        subCitys = unboxer.unbox(key: "sub")
    }
}

簡單的說明一下這個model股淡,這里繼承NSObject的原因是身隐,LeveItemProtocol繼承了NSObjectProtocol,而NSObject對NSObjectProtocol提供了默認實現(xiàn)唯灵,繼承了NSObject也就滿足了NSObjectProtocol的要求贾铝;Unboxable協(xié)議用來json轉(zhuǎn)model,LeveItemProtocol可以讓CityModel做為MultiLevelMenu的數(shù)據(jù)源埠帕。對于LeveItemProtocol的實現(xiàn)垢揩,我們這里是返回當前城市名稱和下一級城市。實際中可能每個項目的model會和這里的有很大差別敛瓷,可以根據(jù)自己的需求進行調(diào)整叁巨。

在ViewModel這個group中,創(chuàng)建一個CityViewModel.swift文件呐籽,我們將在這里把json數(shù)據(jù)轉(zhuǎn)成我們可以使用的model锋勺,在這個文件中蚀瘸,添加以下代碼。

import Foundation
import Unbox

class CityViewModel {
    
    lazy var cityArray: [CityModel]? = self.convertJSONDataToCityModel()
    
    private func convertJSONDataToCityModel() -> [CityModel]? {
        guard let filePath = Bundle.main.url(forResource: "china-city-info", withExtension: "json"),
            let data = try? Data(contentsOf: filePath) else {
                return nil
        }
        
        do {
            let cityArray: [CityModel] = try unbox(data: data)
            return cityArray
        } catch {
            print(error.localizedDescription)
        }
        
        return nil
    }
    
}

一個對外公開的懶惰變量庶橱,通過這個變量獲取到城市數(shù)組贮勃。實際工作由func convertJSONDataToCityModel()方法完成,在這里我們得到城市數(shù)據(jù)文件的路徑苏章,再用Data的初始化方法得到Data類型的數(shù)據(jù)寂嘉,最后使用unbox的初始化方法,將json數(shù)據(jù)轉(zhuǎn)化為城市數(shù)組布近。在路徑錯誤垫释,或者轉(zhuǎn)換失敗的情況下都會返回nil丝格。

現(xiàn)在來看看我們是否正確的獲取到了城市數(shù)據(jù)撑瞧,切換到HomeViewController.swift文件,在viewDidLoad()方法的最后添加兩行代碼显蝌,并在最后的花括號中打個斷點预伺。

運行到斷點處,查看控制臺曼尊,可以看到我們確實正確的獲得了城市數(shù)據(jù)酬诀。

刪掉剛剛添加的兩行代碼和斷點。接著我們切換到MultiLevelMenu.swift文件骆撇,我們需要為MultiLevelMenuDataSource聲明一個新的數(shù)據(jù)方法func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]?瞒御,MultiLevelMenu將通過這個數(shù)據(jù)源方法獲得真正展示的數(shù)據(jù)。

@objc protocol MultiLevelMenuDataSource: NSObjectProtocol {
    @objc func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int
    @objc func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]?
}

從方法名可以大概知道神郊,這個方法讓外界返回需要展示的第一級數(shù)據(jù)肴裙,為什么只要第一數(shù)據(jù)就夠了呢?涌乳?因為這里的每一級數(shù)據(jù)都實現(xiàn)了LeveItemProtocol蜻懦,所以我們能夠通過nextLevelItems屬性來得到下一級需要展示的數(shù)據(jù)。

在我們聲明這個數(shù)據(jù)源方法的同時夕晓,Xcode也報錯了宛乃,告訴我們HomeViewController沒有遵守MultiLevelMenuDataSource協(xié)議。那我們就回到HomeViewController.swift文件蒸辆,實現(xiàn)這個數(shù)據(jù)源方法征炼。在HomeViewController的extension部分,添加這個數(shù)據(jù)源方法躬贡,在方法中谆奥,我們返回通過CityViewModel得到的城市數(shù)組數(shù)據(jù)。

func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]? {
    return CityViewModel().cityArray
}

現(xiàn)在回到MultiLevelMenu.swift文件逗宜,為MultiLevelMenu類添加以下兩個屬性雄右。

fileprivate var firstLevelItems: [LeveItemProtocol]?
fileprivate lazy var selectedLevelItems = [LeveItemProtocol?]()
  • firstLevelItems 記錄第一級數(shù)據(jù)空骚,通過第一級數(shù)據(jù)可以獲取后續(xù)級別的數(shù)據(jù),所及我們對第一級數(shù)據(jù)做一個引用
  • selectedLevelItems 記錄選中的級別項擂仍,比如選中北京囤屹,海淀區(qū),那么第一項是北京逢渔,第二項是海淀區(qū)肋坚,對應(yīng)城市的第一級和第二級。

現(xiàn)在假設(shè)我們的MultiLevelMenu已經(jīng)得到了第一級城市數(shù)據(jù)肃廓,我們還要展示第二級和第三級數(shù)據(jù)智厌,因此我們需要對得到的第一級數(shù)據(jù)做一些處理。添加一個func handleLevelData()方法處理數(shù)據(jù)盲赊。在這里我們默認選中每一級的第一項铣鹏,這就是for循環(huán)的作用。

private func handleLevelData() {
    guard let firstLevelItems = firstLevelItems else {
        return
    }
    
    for level in 0..<numberOfLvel {
        if selectedLevelItems.count == 0 {
            selectedLevelItems.append(firstLevelItems[0])
        } else {
            if let nextLevelItems = selectedLevelItems[level - 1]?.nextLevelItems {
                selectedLevelItems.append(nextLevelItems[0])
            } else {
                selectedLevelItems.append(nil)
            }
        }
    }
}

接下來我們需要對MultiLevelMenu類進行兩處修改哀蘑。首先讓我們我們找到dataSource這個屬性诚卸,修改它的屬性觀察者。

weak var dataSource: MultiLevelMenuDataSource? {
    didSet {
        guard let dataSource = dataSource else {
            return
        }
        
        numberOfLvel = dataSource.numberOfLevel(of: self)
        firstLevelItems = dataSource.firstLevelItems(of: self)
        handleLevelData()
        createMenuTableView()
    }
}

數(shù)據(jù)已經(jīng)處理好绘迁,下一步我們修改tableview的數(shù)據(jù)源方法合溺,將數(shù)據(jù)展示出來。修改如下缀台。

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    let level = tableView.tag - 100
    if level == 0 {
        return firstLevelItems?.count ?? 0
    } else {
        return selectedLevelItems[level - 1]?.nextLevelItems?.count ?? 0
    }
}
    
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    
    let level = tableView.tag - 100
    
    var levelItem: LeveItemProtocol?
    if level == 0 {
        levelItem = firstLevelItems?[indexPath.row]
    } else {
        levelItem = selectedLevelItems[level - 1]?.nextLevelItems?[indexPath.row]
    }
    
    let cell = tableView.dequeueReusableCell(withIdentifier: LevelItemCellReuseIdentifier, for: indexPath) as! LevelItemCell
    
    cell.levelNamelabel.text = levelItem?.levelName
    
    return cell
}

我們通過選中的級別來判斷需要顯示在tableview中多少行棠赛,在這里第一級別為省,我們直接返回第一級別的數(shù)目膛腐,對于第二級別(相對于省來說為市這個級別)睛约,我們需要根據(jù)選中的第一級別來判斷需要顯示多少行,通過上一級的nextLevelItems屬性依疼,可以獲得該級別的所有下一級項痰腮,我們返回所有下一級的數(shù)量。對于第三級別也用同樣的方法得到需要顯示的行數(shù)律罢。得到下一級數(shù)量的邏輯是在else子句中膀值。

有了行數(shù)之后,我們簡單的對cell配置误辑,我們根據(jù)當前顯示的級別沧踏,獲得需要顯示的條目,這部分邏輯和獲得行數(shù)的邏輯是幾乎一樣的巾钉,最后我們將級別的名稱設(shè)置到cell上翘狱。

運行一遍看看效果。

可以看到我們正確的展示了城市數(shù)據(jù)砰苍,目前默認顯示的是北京地區(qū)潦匈。只是現(xiàn)在還有一些問題阱高,點擊任何一個城市并沒有更新對應(yīng)的區(qū)域。現(xiàn)在我們就來解決這個問題茬缩。

滾動到文件的末尾赤惊,找到MultiLevelMenu的UITableViewDelegate這個extension部分,我們在這里實現(xiàn)func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)代理方法凰锡,這個方法在tableview的某一行被選中時會被調(diào)用未舟。添加以下代碼段。

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let level = tableView.tag - 100
    updateNextLevelItems(selectedLevel: level, on: indexPath)
}
    
// 選中某一級別掂为,需要更新對應(yīng)的下一級別
func updateNextLevelItems(selectedLevel: Int, on indexPath: IndexPath) {
    // 更新對應(yīng)的下一級別
    if selectedLevel == 0 {
        selectedLevelItems[0] = firstLevelItems?[indexPath.row]
    } else {
        selectedLevelItems[selectedLevel] = selectedLevelItems[selectedLevel - 1]?.nextLevelItems?[indexPath.row]
    }
    
    // 后續(xù)選中的下一級別默認為第一項
    for level in (selectedLevel+1)..<numberOfLvel {
        selectedLevelItems[level] = selectedLevelItems[level - 1]?.nextLevelItems?.first
    }
    
    // 刷新后續(xù)的tableview
    for level in (selectedLevel+1)..<numberOfLvel {
        menuTableViews[level].reloadData()
    }
}

在這里我們對選中級別做了更新裕膀,并且刷新對應(yīng)的tableview。完成之后勇哗,再次運行程序昼扛,現(xiàn)在選中任何城市,都能夠顯示對應(yīng)的城市信息智绸。下圖是選中廣東省野揪,珠海市的結(jié)果访忿。

到這里為止瞧栗,我們大部分的工作已經(jīng)完成了。接下來我們還可以為MultiLevelMenu聲明代理協(xié)議海铆,通過這些代理協(xié)議迹恐,我們可以制定MultiLevelMenu的外觀,以及告知外界我們選中的級別信息卧斟。找到MultiLevelMenuDataSource數(shù)據(jù)源協(xié)議聲明的位置殴边,在它的下方添加MultiLevelMenuDelegate代理協(xié)議的聲明。

@objc protocol MultiLevelMenuDelegate: NSObjectProtocol {
    
    @objc optional func multiLevelMenu(multiLevelMenu: MultiLevelMenu, backgroundColorForLevel level: Int) -> UIColor
    @objc optional func multiLevelMenu(multiLevelMenu: MultiLevelMenu, widthRatioForLevel level: Int) -> CGFloat
    
    @objc optional func multiLevelMenu(multiLevelMenu: MultiLevelMenu, didSelectedLastLevel selectedLevelItems: [LeveItemProtocol])
}

這個三個代理方法都是可選的珍语,沒有必要全部實現(xiàn)锤岸。從上之下每個方法的作用為:

  • 從外界得到每一級別tableview的背景色
  • 得到每一級別tableview的顯示寬度比例
  • 告知外界我們選中的級別

代理協(xié)議聲明好之后,我們?yōu)镸ultiLevelMenu添加一個delegate變量板乙,以及兩個方法是偷,分別用來改變tableview的背景色和顯示寬度,這兩個方法會在從外界獲得背景色和顯示寬度比例時被調(diào)用募逞。

首先聲明delegate變量

weak var delegate: MultiLevelMenuDelegate?

再添加調(diào)整背景色和寬度的方法

private func changeTableViewBackgroudColor() {
    for level in 0..<numberOfLvel {
        let backgroundColor = self.delegate!.multiLevelMenu!(multiLevelMenu: self, backgroundColorForLevel: level)
        menuTableViews[level].backgroundColor = backgroundColor
    }
}
    
private func remakeTableViewConstraints() {
    for level in 0..<numberOfLvel {
        let widthRatio = self.delegate!.multiLevelMenu!(multiLevelMenu: self, widthRatioForLevel: level)
        let tableView = menuTableViews[level]
        
        tableView.snp.remakeConstraints({ (make) in
            make.top.bottom.equalToSuperview()
            make.width.equalToSuperview().multipliedBy(widthRatio)
            
            if level == 0 {
                make.leading.equalToSuperview()
            } else {
                make.leading.equalTo(self.menuTableViews[level - 1].snp.trailing)
            }
        })
    }
}

在這兩個方法中蛋铆,我們對delegate和可選方法進行了強制解包,這里本來應(yīng)該做判空處理的放接,待會我們就可以知道為什么我們不需要在這里做判空處理刺啦。我們調(diào)整tableview的寬度,是通過設(shè)置寬度約束來實現(xiàn)的纠脾。

現(xiàn)在我們找到delegate變量玛瘸,為它增加屬性觀察器.

weak var delegate: MultiLevelMenuDelegate? {
    didSet {
        guard let delegate = delegate else {
            return
        }
        
        if delegate.responds(to: #selector(MultiLevelMenuDelegate.multiLevelMenu(multiLevelMenu:backgroundColorForLevel:))) {
            changeTableViewBackgroudColor()
        }
        
        if delegate.responds(to: #selector(MultiLevelMenuDelegate.multiLevelMenu(multiLevelMenu:backgroundColorForLevel:))) {
            remakeTableViewConstraints()
        }
    }
}

在delegate變量被設(shè)置之后蜕青,我們?nèi)ジ淖僼ableview的背景色和顯示寬度,我們在這里做了判空操作糊渊,這就是為什么我們不需要在前面兩個方法中進行判空操作的原因市咆。

切換到HomeViewController.swift文件,為HomeViewController添加MultiLevelMenuDelegate協(xié)議的實現(xiàn)再来。

extension HomeViewController: MultiLevelMenuDelegate {
    func multiLevelMenu(multiLevelMenu: MultiLevelMenu, backgroundColorForLevel level: Int) -> UIColor {
        if level == 0 {
            return UIColor(white: 0.92, alpha: 1.0)
        } else if level == 1 {
            return UIColor(white: 0.94, alpha: 1.0)
        } else {
            return UIColor(white: 0.96, alpha: 1.0)
        }
    }
    
    func multiLevelMenu(multiLevelMenu: MultiLevelMenu, widthRatioForLevel level: Int) -> CGFloat {
        if level == 0 {
            return 0.24
        } else if level == 1 {
            return 0.38
        } else {
            return 0.38
        }
    }
}

這里的顏色和寬度比例可以根據(jù)需求調(diào)整蒙兰。最后不要忘記設(shè)置MultiLevelMenu對象的delegate屬性為self,找的multiLevelMenu的聲明處芒篷,添加一行代碼搜变。

multiLevelMenu.delegate = self

最后運行我們的程序看看效果,看起來效果還是可以的针炉。

接下來我們實現(xiàn)MultiLevelMenuDelegate協(xié)議的最后一個代理方法

在我們剛剛的extension部分挠他,實現(xiàn)最后一個代理方法,這里我們只是簡單的打印級別名稱篡帕。

func multiLevelMenu(multiLevelMenu: MultiLevelMenu, didSelectedLastLevel selectedLevelItems: [LeveItemProtocol]) {
    for levelItem in selectedLevelItems {
        print(levelItem.levelName)
    }
}

回到MultiLevelMenu.swift文件殖侵,是時候告知外界我們選中的級別信息了。修改tableview選中某一行的代理方法镰烧。

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    let level = tableView.tag - 100
    updateNextLevelItems(selectedLevel: level, on: indexPath)
    
    // 檢查是否選中了最后一個級別拢军,并對delegate和可選方法進行判空操作
    if (selectedLevelItems[level]?.nextLevelItems == nil)
        && (delegate != nil)
        && (delegate!.responds(to: #selector(MultiLevelMenuDelegate.multiLevelMenu(multiLevelMenu:didSelectedLastLevel:)))) {
        delegate?.multiLevelMenu!(multiLevelMenu: self, didSelectedLastLevel: selectedLevelItems.filter{ $0 != nil } as! [LeveItemProtocol])
        dismiss(animated: true)
    }
}

在這里如果我們選中了最后一級就告訴外界我們選中的級別,并移除MultiLevelMenu怔鳖。運行看看效果茉唉,這里我第一次選中了北京的朝陽區(qū),第二次選中了廣東佛山的三水區(qū)结执,都正確的在控制臺打印出來了度陆。

我們還可以為MultiLevelMenu的展現(xiàn)和移除添加動畫。首先我們找到presnt(from view: UIView)方法献幔,在這個方法的最后懂傀,添加以下幾行代碼。

// 1.改變約束讓containerView的位置剛好在MultiLevelMenu控件的上方蜡感,并使約束立即生效
self.containerView.snp.remakeConstraints { (make) in
    make.leading.trailing.equalToSuperview()
    make.height.equalTo(240)
    make.bottom.equalTo(self.snp.top)
}
self.layoutIfNeeded()
self.alpha = 0.0
    
// 2.恢復(fù)containerView到原來的位置蹬蚁,使用動畫讓約束逐步生效
self.containerView.snp.remakeConstraints { (make) in
    make.leading.top.trailing.equalToSuperview()
    make.height.equalTo(240)
}
    
UIView.animate(withDuration: CATransaction.animationDuration()) { 
    self.alpha = 1.0
    self.layoutIfNeeded()
}

我們這里設(shè)置通過讓containerView從上往下移動,以及改變MultiLevelMenu控件的透明度铸敏,產(chǎn)生一個動畫缚忧。

我們還需要在MultiLevelMenu的類的初始化中添加一行代碼,它讓超出MultiLevelMenu控件的內(nèi)容不可見杈笔。

self.clipsToBounds = true

現(xiàn)在修改func dismiss(animated: Bool)方法闪水,并添加處理觸摸事件的方法。

func dismiss(animated: Bool) {
    if animated {
        UIView.animate(withDuration: CATransaction.animationDuration(), animations: { 
            self.alpha = 0.0
        }, completion: { (_) in
            self.removeFromSuperview()
        })
    } else {
        self.removeFromSuperview()
    }
    
    isShowed = false
}
    
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    dismiss(animated: true)
}

在移除MultiLevelMenu控件時,我們先將它的透明度變?yōu)?球榆,完成之后再移除朽肥。運行看看效果吧,到這里我們的整個demo就算是完成了????持钉。

為了方便對照衡招,可以在這里下載demo。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末每强,一起剝皮案震驚了整個濱河市始腾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌空执,老刑警劉巖浪箭,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異辨绊,居然都是意外死亡奶栖,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門门坷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宣鄙,“玉大人,你說我怎么就攤上這事默蚌《澄睿” “怎么了?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵敏簿,是天一觀的道長明也。 經(jīng)常有香客問我,道長惯裕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任绣硝,我火速辦了婚禮蜻势,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘鹉胖。我一直安慰自己握玛,他們只是感情好,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布甫菠。 她就那樣靜靜地躺著挠铲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪寂诱。 梳的紋絲不亂的頭發(fā)上拂苹,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音痰洒,去河邊找鬼瓢棒。 笑死浴韭,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的脯宿。 我是一名探鬼主播念颈,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼连霉!你這毒婦竟也來了榴芳?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤跺撼,失蹤者是張志新(化名)和其女友劉穎翠语,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體财边,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡肌括,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了酣难。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片谍夭。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖憨募,靈堂內(nèi)的尸體忽然破棺而出紧索,到底是詐尸還是另有隱情,我是刑警寧澤菜谣,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布珠漂,位于F島的核電站,受9級特大地震影響尾膊,放射性物質(zhì)發(fā)生泄漏媳危。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一冈敛、第九天 我趴在偏房一處隱蔽的房頂上張望待笑。 院中可真熱鬧,春花似錦抓谴、人聲如沸暮蹂。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽仰泻。三九已至,卻和暖如春滩届,著一層夾襖步出監(jiān)牢的瞬間集侯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留浅悉,地道東北人趟据。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像术健,于是被迫代替她去往敵國和親汹碱。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 大多數(shù)時候荞估,我們說的越多咳促,彼此的距離越遠,矛盾也越多勘伺。在溝通中跪腹,大多數(shù)人總是急于表達自己,一吐為快飞醉,卻一點也不懂對...
    無怨無悔的灑脫閱讀 211評論 0 0
  • 【看譯文才懂的句子】 1. “And even the miserable lives we lead are n...
    若湖_yuki閱讀 780評論 0 2
  • 罐頭之所以能夠長期保存而不變質(zhì)冲茸,完全得益于密封的容器和嚴格的殺菌,與防腐劑毫無關(guān)系缅帘。 罐頭其實并不神秘轴术,罐頭的原理...
    筱小麗閱讀 409評論 0 4
  • 曾經(jīng)有一段時間逗栽,我覺得自己是不配擁有的愛情的。 一段好的感情失暂,應(yīng)當是兩個人在彼此的感情里相互成長彼宠,變得更好。...
    卡卡kk閱讀 374評論 0 0