为接下来我们要做什么有个底,先来看看完成后的效果图。总的来说我们要实现这样一个下拉控件,点击城市按钮,就会弹出城市选择下拉列表,点击第一列或者第二列就会根据点击的行刷新后面的列表,比如点击了省份这一列会更新相应的城市列表和区县列表,大概可以用级联更新这个词来描述吧!在点击最后一列后列表会消失,点击灰色区域城市列表也会消失,在列表展示的情况下,点击城市按钮,城市列表也会消失。停止哔哔🤣😂,撸起袖子开始干!
我们这次的重点是多级下拉列表的开发,因此我们从一个初始项目开始,直接进入下拉列表的开发的开发环节🤡🤡。从这里下载我们的初始项目,下图是初始项目运行后的结果。
做一个小小的说明:
- 这次我们项目中使用SnapKit以纯代码的方式进行界面布局,不熟悉的朋友不同担心,我们的Demo项目界面比较简单,布局代码也是简洁明了的。
- 另外城市列表数据格式为json,我们通过Unbox第三方库,将json数据转成项目中可以使用的Model
- 这两个库已经通过pod安装,包含在初始项目中,初始列表还包括城市列表数据
从此刻开始我们的多级下拉控件叫做MultiLevelMenu
我们使用多个UITableView来实现多个列表,从完成效果图来看,这里有3个UITableView对象,可是为了把这个MultiLevelMenu控件做成一个通用控件,我们需要让外界提供给数据给MultiLevelMenu,然后进行展示。类似UITableView的数据源方法,我们也会创建DataSource 协议,让外界通过这个协议为MultiLevelMenu提供数据。
好开心,好开心,终于可以敲代码了😈😈😈!
打开项目文件导航栏,选中View这个Group,(command + n)创建一个UIView的子类MultiLevelMenu,这个就是我们的多级下拉列表啦!刚刚的操作可以看一看下面的步骤截图。
打开MultiLevelMenu.swift文件,添加初始化方法,在这里我们将背景色设置为黑色,透明度为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定义的上方,添加一个数据源协议MultiLevelMenuDataSource,声明如下
@objc protocol MultiLevelMenuDataSource: NSObjectProtocol {
@objc func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int
}
现在MultiLevelMenuDataSource声明中只有一个方法,外界通过这个数据源方法,告诉MultiLevelMenu要显示多少个级别的数据,比如返回3,那么就会有3个列表。MultiLevelMenu通过这个数据源方法,配置列表和数据。
数据源协议声明好了,我们在MultiLevelMenu类中添加一个dataSource属性,代表数据源。
weak var dataSource: MultiLevelMenuDataSource?
暂时把数据源放一放,为了在table view上展示城市信息,定义一个UITableViewCell的子类LevelItemCell,在MultiLevelMenuDataSource的声明下面定义这个类。这个cell非常简单,只包含了一个label控件,设置了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中,方便管理和实现一些动画效果。在menuTableViews变量的下方,创建这个view
private lazy var containerView: UIView = {
let view = UIView()
return view
}()
接下来我们在MultiLevelMenu类中添加两个私有方法,用来创建展示城市数据的table view,以及对table view进行布局。
// 创建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)
}
}
创建table view这个方法很好懂,在这里对table view做了一些常规的配置,由于我们还没有实现UITableView的数据源方法和delegate方法,这里编译器会有错误提示,这个不要紧,我们后面加上去。
在布局table view的方法中,为每个table view添加约束,table view的top和bottom和containerView一致;width为containerView的1.0 / numberOfLvel,也就是说如果有3个级别,那么每个列表的宽度为containerView的1/3;table view的左边的约束设置与containerView的左边相等或者与前一级列表的右边相等。最后设置containerView的左边,上边,右边约束与MultiLevelMenu对象本身相等,height约束为240,这个可以根据需要改变。希望我有解释清楚这个方法😂😂。
现在我们使用extension的方式为MultiLevelMenu添加UITableView的数据源方法和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正确的展示出来。
我们为MultiLevelMenu的dataSource属性添加一个属性观察器,当dataSource被设置之后,我们就调用数据源方法numberOfLevel(of multiLevelMenu: MultiLevelMenu),获得需要展示多少个级别的信息并创建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")
}
// 创建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
}()
最后实现MultiLevelMenu的数据源方法,告诉MultiLevelMenu需要展示多少级数据。
extension HomeViewController: MultiLevelMenuDataSource {
func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int {
return 3
}
}
🐶激动人心的时刻来啦,运行一次看看效果🐹。点击城市按钮之后,可以看到我们已经将MultiLevelMenu展示出来了🐵🐼。
这里我们是直接把MultiLevelMenu添加到当前controller的view上的,接下来我们换一种方式来展示MultiLevelMenu,在UIWindow上添加MultiLevelMenu。切换到MultiLevelMenu.swift文件,添加一个变量用来记录MultiLevelMenu是否已经弹出,再添加两个方法,分别用来弹出和移除MultiLevelMenu。
- func presnt(from view: UIView),从一个view的下方弹出MultiLevelMenu,比如传进来一个button,那么将在这个button的下方显示MultiLevelMenu控件。
- func dismiss(animated: Bool) 将MultiLevelMenu控件从UIWindow移除。
- var isShowed = false,记录MultiLevelMenu的显示状态
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的。假如我们现在从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没有弹出,我们就显示它,如果已经显示,我们就移除它。
@objc private func presentMultiLevelMunu(sender: UIButton) {
multiLevelMenu.isShowed ? multiLevelMenu.dismiss(animated: true) : multiLevelMenu.presnt(from: sender)
}
运行一遍试试吧😁😁!我们正确的将MultiLevelMenu,显示在cityButton的下方,而且显示,移除都没有问题。
接下来我们需要为MultiLevelMenu,填充点真实的数据了,这部分才是我们真正的重点,所以打起精神吧少年!!。
看看我们每一级的table view需要显示什么数据,可以发现只要显示某个level的名称就够了,选中一个level就会显示所有下一级的名称,因此我们可以定义一个Level类,来代表每一个需要显示的Level节点。
class Level {
var levelName = ""
var netLevelItems: [Level]?
}
我们可以让具体的Model去继承Level类,在MultiLevelMenu的使用上只需要知道levelName和对应的下一级就够了,所以MultiLevelMenu能够把Level类或者它的子类作为自己的数据源。
为了践行面向协议编程的思想,这里不使用继承的方式实现MultiLevelMenu的数据源结构,而将Level类以协议的方式实现。在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格式的城市信息数据,这里会使用到Unbox第三方库将JSON数据转为模型。
在Model这个group中新建一个swift文件,命名为CityModel.swift,将文件内容替换为下面的代码段。
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提供了默认实现,继承了NSObject也就满足了NSObjectProtocol的要求;Unboxable协议用来json转model,LeveItemProtocol可以让CityModel做为MultiLevelMenu的数据源。对于LeveItemProtocol的实现,我们这里是返回当前城市名称和下一级城市。实际中可能每个项目的model会和这里的有很大差别,可以根据自己的需求进行调整。
在ViewModel这个group中,创建一个CityViewModel.swift文件,我们将在这里把json数据转成我们可以使用的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
}
}
一个对外公开的懒惰变量,通过这个变量获取到城市数组。实际工作由func convertJSONDataToCityModel()方法完成,在这里我们得到城市数据文件的路径,再用Data的初始化方法得到Data类型的数据,最后使用unbox的初始化方法,将json数据转化为城市数组。在路径错误,或者转换失败的情况下都会返回nil。
现在来看看我们是否正确的获取到了城市数据,切换到HomeViewController.swift文件,在viewDidLoad()方法的最后添加两行代码,并在最后的花括号中打个断点。
运行到断点处,查看控制台,可以看到我们确实正确的获得了城市数据。
删掉刚刚添加的两行代码和断点。接着我们切换到MultiLevelMenu.swift文件,我们需要为MultiLevelMenuDataSource声明一个新的数据方法func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]?,MultiLevelMenu将通过这个数据源方法获得真正展示的数据。
@objc protocol MultiLevelMenuDataSource: NSObjectProtocol {
@objc func numberOfLevel(of multiLevelMenu: MultiLevelMenu) -> Int
@objc func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]?
}
从方法名可以大概知道,这个方法让外界返回需要展示的第一级数据,为什么只要第一数据就够了呢??因为这里的每一级数据都实现了LeveItemProtocol,所以我们能够通过nextLevelItems属性来得到下一级需要展示的数据。
在我们声明这个数据源方法的同时,Xcode也报错了,告诉我们HomeViewController没有遵守MultiLevelMenuDataSource协议。那我们就回到HomeViewController.swift文件,实现这个数据源方法。在HomeViewController的extension部分,添加这个数据源方法,在方法中,我们返回通过CityViewModel得到的城市数组数据。
func firstLevelItems(of multiLevelMenu: MultiLevelMenu) -> [LeveItemProtocol]? {
return CityViewModel().cityArray
}
现在回到MultiLevelMenu.swift文件,为MultiLevelMenu类添加以下两个属性。
fileprivate var firstLevelItems: [LeveItemProtocol]?
fileprivate lazy var selectedLevelItems = [LeveItemProtocol?]()
- firstLevelItems 记录第一级数据,通过第一级数据可以获取后续级别的数据,所及我们对第一级数据做一个引用
- selectedLevelItems 记录选中的级别项,比如选中北京,海淀区,那么第一项是北京,第二项是海淀区,对应城市的第一级和第二级。
现在假设我们的MultiLevelMenu已经得到了第一级城市数据,我们还要展示第二级和第三级数据,因此我们需要对得到的第一级数据做一些处理。添加一个func handleLevelData()方法处理数据。在这里我们默认选中每一级的第一项,这就是for循环的作用。
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()
}
}
数据已经处理好,下一步我们修改tableview的数据源方法,将数据展示出来。修改如下。
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中多少行,在这里第一级别为省,我们直接返回第一级别的数目,对于第二级别(相对于省来说为市这个级别),我们需要根据选中的第一级别来判断需要显示多少行,通过上一级的nextLevelItems属性,可以获得该级别的所有下一级项,我们返回所有下一级的数量。对于第三级别也用同样的方法得到需要显示的行数。得到下一级数量的逻辑是在else子句中。
有了行数之后,我们简单的对cell配置,我们根据当前显示的级别,获得需要显示的条目,这部分逻辑和获得行数的逻辑是几乎一样的,最后我们将级别的名称设置到cell上。
运行一遍看看效果。
可以看到我们正确的展示了城市数据,目前默认显示的是北京地区。只是现在还有一些问题,点击任何一个城市并没有更新对应的区域。现在我们就来解决这个问题。
滚动到文件的末尾,找到MultiLevelMenu的UITableViewDelegate这个extension部分,我们在这里实现func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath)代理方法,这个方法在tableview的某一行被选中时会被调用。添加以下代码段。
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let level = tableView.tag - 100
updateNextLevelItems(selectedLevel: level, on: indexPath)
}
// 选中某一级别,需要更新对应的下一级别
func updateNextLevelItems(selectedLevel: Int, on indexPath: IndexPath) {
// 更新对应的下一级别
if selectedLevel == 0 {
selectedLevelItems[0] = firstLevelItems?[indexPath.row]
} else {
selectedLevelItems[selectedLevel] = selectedLevelItems[selectedLevel - 1]?.nextLevelItems?[indexPath.row]
}
// 后续选中的下一级别默认为第一项
for level in (selectedLevel+1)..<numberOfLvel {
selectedLevelItems[level] = selectedLevelItems[level - 1]?.nextLevelItems?.first
}
// 刷新后续的tableview
for level in (selectedLevel+1)..<numberOfLvel {
menuTableViews[level].reloadData()
}
}
在这里我们对选中级别做了更新,并且刷新对应的tableview。完成之后,再次运行程序,现在选中任何城市,都能够显示对应的城市信息。下图是选中广东省,珠海市的结果。
到这里为止,我们大部分的工作已经完成了。接下来我们还可以为MultiLevelMenu声明代理协议,通过这些代理协议,我们可以制定MultiLevelMenu的外观,以及告知外界我们选中的级别信息。找到MultiLevelMenuDataSource数据源协议声明的位置,在它的下方添加MultiLevelMenuDelegate代理协议的声明。
@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])
}
这个三个代理方法都是可选的,没有必要全部实现。从上之下每个方法的作用为:
- 从外界得到每一级别tableview的背景色
- 得到每一级别tableview的显示宽度比例
- 告知外界我们选中的级别
代理协议声明好之后,我们为MultiLevelMenu添加一个delegate变量,以及两个方法,分别用来改变tableview的背景色和显示宽度,这两个方法会在从外界获得背景色和显示宽度比例时被调用。
首先声明delegate变量
weak var delegate: MultiLevelMenuDelegate?
再添加调整背景色和宽度的方法
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和可选方法进行了强制解包,这里本来应该做判空处理的,待会我们就可以知道为什么我们不需要在这里做判空处理。我们调整tableview的宽度,是通过设置宽度约束来实现的。
现在我们找到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变量被设置之后,我们去改变tableview的背景色和显示宽度,我们在这里做了判空操作,这就是为什么我们不需要在前面两个方法中进行判空操作的原因。
切换到HomeViewController.swift文件,为HomeViewController添加MultiLevelMenuDelegate协议的实现。
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
}
}
}
这里的颜色和宽度比例可以根据需求调整。最后不要忘记设置MultiLevelMenu对象的delegate属性为self,找的multiLevelMenu的声明处,添加一行代码。
multiLevelMenu.delegate = self
最后运行我们的程序看看效果,看起来效果还是可以的。
接下来我们实现MultiLevelMenuDelegate协议的最后一个代理方法
在我们刚刚的extension部分,实现最后一个代理方法,这里我们只是简单的打印级别名称。
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。运行看看效果,这里我第一次选中了北京的朝阳区,第二次选中了广东佛山的三水区,都正确的在控制台打印出来了。
我们还可以为MultiLevelMenu的展现和移除添加动画。首先我们找到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.恢复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()
}
我们这里设置通过让containerView从上往下移动,以及改变MultiLevelMenu控件的透明度,产生一个动画。
我们还需要在MultiLevelMenu的类的初始化中添加一行代码,它让超出MultiLevelMenu控件的内容不可见。
self.clipsToBounds = true
现在修改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控件时,我们先将它的透明度变为0,完成之后再移除。运行看看效果吧,到这里我们的整个demo就算是完成了😃😃。
为了方便对照,可以在这里下载demo。