如何在自定义 Tool Bar 和 Tab Bar 之间切换显示

3,917 阅读9分钟

UITabBarController 结合 UINavigationController、UITableViewController,在 iOS App 的 UI 设计中是比较经典的组合用法,效果可以参考原生电话 App。

本文我们要实现的是,在点击导航栏的按钮后,隐藏 TabBar,显示自定义的工具栏菜单,再次点击按钮切换回来。

本文的 示例工程 已上传至 Github,欢迎下载调试,完成后 App 显示效果如下:

4
4

12
12

下面我们从头开始创建示例工程:

1.首先编程环境使用 Xcode 9.1 版本、Swift 4.0 语言,支持 iOS 10,新建工程 Single View App -> 工程名: SwitchBetweenCustomToolBarAndTabBar
2.新建文件,选择 UITableViewController 模版,命名为 MainTableViewController
3.再新建一个 UITableViewController 模版,命名为 DetailTableViewController
4.在 Main.storyboard 中,删除默认视图,拖拽两个 Table View Controller,分别关联至刚才创建的 MainTableViewControllerDetailTableViewController
5.选择 MainTableViewController,在菜单栏选择 Editor -> Embed in -> Navigation Controller ,效果如下:

1
1

6.在 StoryBoard 上对两个表格做些基本设置,第一个表格设置为 Dynamic PrototypesGrouped1 Rows。选中 Cell,选择 style Right Detail,设置 IdentifierbookCell,Accessory 选择 Disclosure Indicator。选中视图上的 Navigation Item,设置标题为:“书籍列表”。再拖入一个 Bar Button Item 放在导航栏右侧,命名为:“编辑”。

7.第二个表格设置为 Static CellsGrouped1 Sections3 Rows,再拖拽一个 Navigation Item,并设置标题为:“书籍详情”。选中 Cell,选择 style Right Detail,修改标签名称。在两个表格之间创建一个 Selection Segue,选中第一个表格的 Cell,按住 control 连接至第二个表格,在 Selection Segue 下选择 Show,选择 Segue,设置 IdentifiershowDetail。效果如下:

2
2

8.在 StoryBoard 中再拖入一个 Tab Bar Controller,默认自带两个标签页,选中 Tab Bar Controller,按住 control 连接至 Navigation Controller ,选择 Relationship Segue 下的 view controllers。选中 Tab Bar 中的标签图标,移动下标签顺序,拖动即可,将我们要展示的表格放在前面。选中 Navigation Controller 中的 Item,修改 title 为“书籍列表”。设置下每个标签的图标。最后选中 Tab Bar Controller,勾选 Is Initial View Controller。现在效果如下:

3
3

9.现在来做点代码工作。打开 MainTableViewController,添加编辑按钮的 IBOutlet、添加初始数据、完善数据源方法等,代码如下:

    // MARK: 1.--@IBOutlet属性定义-----------👇
    @IBOutlet weak var editButton: UIBarButtonItem!


    // MARK: 2.--实例属性定义----------------👇
    var bookList = [
        ["name": "读库","author": "张立宪", "press": "新星出版社"],
        ["name": "三体","author": "刘慈欣", "press": "重庆出版社"],
        ["name": "驱魔","author": "韩松", "press": "上海文艺出版社"],
        ["name": "叶曼拈花","author": "叶曼", "press": "中央编译出版社"],
        ["name": "南华录 : 晚明南方士人生活史","author": "赵柏田", "press": "北京大学出版社"],
        ["name": "青鸟故事集","author": "李敬泽", "press": "译林出版社"],
        ["name": "可爱的文化人","author": "俞晓群", "press": "岳麓书社"],
        ["name": "呼吸 : 音乐就在我们的身体里","author": "杨照", "press": "广西师范大学出版社"],
        ["name": "书生活","author": "马慧元", "press": "中华书局"],
        ["name": "叶弥六短篇","author": "叶弥", "press": "海豚出版社"],
        ["name": "美哉少年","author": "叶弥", "press": "江苏凤凰文艺出版社"],
        ["name": "新与旧","author": "沈从文", "press": "重庆大学出版社"],
        ["name": "银河帝国:基地","author": "艾萨克·阿西莫夫", "press": "江苏文艺出版社"],
        ["name": "世界上的另一个你","author": "朗·霍尔 丹佛·摩尔", "press": "湖南文艺出版社"],
        ["name": "奇岛","author": "林语堂", "press": "群言出版社"]
    ]

    // MARK: 3.--视图生命周期----------------👇

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.allowsMultipleSelectionDuringEditing = true // 允许编辑模式下多选
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    // MARK: 4.--处理主逻辑-----------------👇

    /// 切换表格的编辑与浏览状态
    func switchEditMode() {
        if tableView.isEditing {
            self.setEditing(false, animated: true) // 结束编辑模式
            editButton.title = "编辑"
        } else {
            self.setEditing(true, animated: true) // 进入编辑模式
            editButton.title = "取消"
        }
    }

    // MARK: 5.--辅助函数-------------------👇

    // MARK: 6.--动作响应-------------------👇
    @IBAction func editButtonTapped(_ sender: Any) {
        switchEditMode()
    }

    // MARK: 7.--事件响应-------------------👇

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {

    }

    override func shouldPerformSegue(withIdentifier identifier: String,
                                     sender: Any?)  -> Bool {
        // 编辑模式下禁止触发 segue
        if tableView.isEditing {
            return false
        } else {
            return true
        }
    }

    // MARK: 8.--数据源方法------------------👇

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int {
        return bookList.count
    }

    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath)
        -> UITableViewCell 
    {
        let cell = tableView.dequeueReusableCell(
            withIdentifier: "bookCell", for: indexPath)
        let row = indexPath.row
        cell.textLabel?.text = bookList[row]["name"]
        cell.detailTextLabel?.text = bookList[row]["author"]

        return cell
    }

10.现在就可以看到浏览状态和编辑多选状态两种效果:

4
4

5
5

11.再完善一下书籍详情页,打开 DetailTableViewController,添加代码如下:

    // MARK: 1.--@IBOutlet属性定义-----------👇

    // MARK: 2.--实例属性定义----------------👇
    var bookDetail = ["name": "","author": "", "press": ""]

    // MARK: 3.--视图生命周期----------------👇

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }
    // MARK: 4.--处理主逻辑-----------------👇

    // MARK: 5.--辅助函数-------------------👇

    // MARK: 6.--动作响应-------------------👇

    // MARK: 7.--事件响应-------------------👇


    // MARK: 8.--数据源方法------------------👇

    override func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    override func tableView(_ tableView: UITableView,
                            numberOfRowsInSection section: Int) -> Int {
        return 3
    }

    override func tableView(_ tableView: UITableView,
                            cellForRowAt indexPath: IndexPath)
        -> UITableViewCell
    {
        let cell = super.tableView(tableView, cellForRowAt: indexPath)
        let row = indexPath.row
        switch row {
        case 0:
            cell.detailTextLabel?.text = bookDetail["name"]
        case 1:
            cell.detailTextLabel?.text = bookDetail["author"]
        case 2:
            cell.detailTextLabel?.text = bookDetail["press"]
        default:
            break
        }

        return cell
    }

    // MARK: 9.--视图代理方法----------------👇

12.回到 MainTableViewController,补充一下 prepare 方法:

    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        let detailVC = segue.destination as! DetailTableViewController
        let cell = sender as! UITableViewCell
        let selectedIndexPath = tableView.indexPath(for: cell)!
        detailVC.bookDetail = bookList[selectedIndexPath.row]
    }

13.现在详情页也能看到内容了:

6
6

14.接下来我们希望在编辑书籍列表的时候,页面底部显示工具栏,方便我们进行删除等操作。Navigation Controller 是自带 Toolbar 的,只需要选中它并勾选 Shows Toolbar 就行,但是默认效果实在是不友好,Toolbar 和 Tabbar 紧挨着,既占空间又不美观,这也是我要写这篇文章的主要原因:

8
8

7
7

15.因此我们接下来要尝试,在编辑时隐藏 Tabbar,只显示 ToolBar,在结束编辑时,隐藏 ToolBar,重新显示 Tabbar。其实,另一个系统自带的 App 已经实现了这个效果,就是照片 App,效果看下图。但是仔细看它也有一个问题,它在点击“选择”按钮时,显示的工具栏是 UIToolbar 类型的,它的高度比 Tabbar 要矮一点,这样在切换时感觉不协调(除了这个问题,iOS 11 上的照片 App 还有其他问题)。

9
9

16.所以我们打算自定义 Toolbar,且要满足以下几个特性:

  • 高度和 Tabbar 一致
  • 颜色一致
  • 上边沿要有根横线
  • 带背景毛玻璃效果

是不是觉得我们要复刻 Tabbar 了?看起来还真有点像,不过我们会做的稍微简单点,看完本文还有想法的可以再去打磨一下。

问题是我们怎么能做的这么像呢?这要多谢 Xcode 的 View Debugging 功能,可以把 Tabbar 刨开来看个够。

17.接下来打开 Xcode,运行一下工程,打开菜单栏:Debug -> View Debugging -> Capture View Hierarchy,可以把 App 视图层次属性看的清清楚楚:

10
10

18.下面我们来逐个实现 Toolbar 需要的特性,先从毛玻璃效果开始。

  • 打开 Xcode 新建 UIVIew 的子类 ToolBarView.swift ,再创建一个 View 的 xib 文件 ToolBarView.xib
  • 在 xib 文件中选中 View ,将 Custom Class 设置为 ToolBarView (这里不在 File's Owner 里设置,很多问答的回复里乱用 File's Owner ,下次专题讲解自定义 UIView 的问题),在 Simulated Metrics 的 Size 项中选择 Freeform,再在尺寸设置中将 View 高度改为 49。
  • 从 UI 模版库中找到 Visual Effect Views With Blur,拖入 View 中,设置约束和 View 保持相同高宽、左上对齐。选中 Visual Effect View,找到设置项 Blur Style ,选择 Extra Light

19.经过前面的观察,Tabbar 上边沿的细横线,其实是一个高度为 0.33、带有背景色的空 Image View,用法是不是很特别。接着我们在 UI 库中找到 Image View,放在 Visual Effect View 上层,并设置约束,高度的约束单独设置为0.33(高度直接在 View 尺寸中设置是不起作用的),其他约束相同。找到 Image ViewBackground 属性,设置为黑色加 30% 透明度。

20.再拖拽一个 Button 到 ToolBarView 上,将约束设置为上下左右居中即可,标题设置为:“删除”。到这一步为止,你应该在 xib 上看到以下层次结构:

11
11

21.打开 ToolBarView.swift,在 Xcode 中创建一个 IBOutlet 关联至“删除”按钮,并添加以下代码:

class ToolBarView: UIView {

    @IBOutlet weak var deleteButton: UIButton!

    class func initView() -> ToolBarView {
        let myClassNib = UINib(nibName: "ToolBarView", bundle: nil)
        let toolBarView = myClassNib.instantiate(
            withOwner: nil,
            options: nil)[0] as! ToolBarView
        return toolBarView
    }

}

22.打开 MainTableViewController.swift,添实例属性:

    /// 工具栏视图
    var toolBarView: ToolBarView?

    /// 编辑状态下选中的书籍数组
    var selectedBooksIndexs: [Int] {
        guard let indexPaths = tableView.indexPathsForSelectedRows else {
            return []
        }
        var indexs: [Int] = []
        for indexPath in indexPaths {
            indexs.append(indexPath.row)
        }
        return indexs
    }

修改 viewDidLoad() 方法如下:

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.allowsMultipleSelectionDuringEditing = true // 允许编辑模式下多选
        initialToolBar() // 初始化工具栏
    }

添加方法 initialToolBar()

    /// 初始化工具栏
    func initialToolBar() {
        toolBarView = ToolBarView.initView() // 初始化工具栏对象
        setupToolBarFrame() // 对工具栏进行布局
        // 添加至 TabBar 视图中
        self.tabBarController?.view.addSubview(toolBarView!)
        toolBarView?.isHidden = true // 默认隐藏
        registerToolBarButtonAction() // 注册按钮点击事件
    }

添加方法 setupToolBarFrame()

    /// 对工具栏进行布局
    func setupToolBarFrame() {
        var frame = CGRect()
        // 工具栏布局与 Tabbar 保持一致
        frame.origin = (self.tabBarController?.tabBar.frame.origin)!
        frame.size = (self.tabBarController?.tabBar.frame.size)!
        toolBarView?.frame = frame
    }

添加方法 registerToolBarButtonAction()

    /// 注册工具栏按钮点击事件
    func registerToolBarButtonAction() {
        // 删除按钮
        toolBarView?.deleteButton.addTarget(
            self, action: #selector(self.deleteToolBarButtonTapped(_:)),
            for: .touchUpInside)
    }

添加方法 deleteToolBarButtonTapped(:)

    /// 响应工具栏删除按钮点击
    @objc func deleteToolBarButtonTapped(_ sender: UIButton) {
        deleteSelectedBooks() // 删除选择的书籍
    }

添加方法 deleteSelectedBooks()

    /// 删除选择的书籍
    func deleteSelectedBooks() {
        let indexs = selectedBooksIndexs.sorted()
        for index in Array(indexs.reversed()) {
            bookList.remove(at: index)
        }
        tableView.beginUpdates()
        tableView.deleteRows(at: indexs.map { IndexPath(row: $0, section: 0) } ,
                             with: .fade)
        tableView.endUpdates()
        switchEditMode()
    }

完善方法 switchEditMode()

    /// 切换表格的编辑与浏览状态
    func switchEditMode() {
        if tableView.isEditing {
            self.setEditing(false, animated: true) // 结束编辑模式
            editButton.title = "编辑"
        } else {
            self.setEditing(true, animated: true) // 进入编辑模式
            editButton.title = "取消"
        }
        switchToolBarAndTabbar() // 切换显示工具栏
    }

添加方法 switchToolBarAndTabbar()

    /// 切换显示工具栏
    func switchToolBarAndTabbar() {
        if tableView.isEditing {
            self.tabBarController?.tabBar.isHidden = true // 隐藏 Tab 栏
            toolBarView?.isHidden = false // 显示工具栏
        } else {
            self.tabBarController?.tabBar.isHidden = false // 显示 Tab 栏
            toolBarView?.isHidden = true // 隐藏工具栏
        }
    }

23.在 Xcode 中运行一下工程,现在就可以愉快地展示自定义 Toolbar 和删除操作了:

12
12

13
13

24.最后再解决一个小问题,设备旋转时需要对工具栏进行重新布局,修改 viewDidLoad() 方法:

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.allowsMultipleSelectionDuringEditing = true // 允许编辑模式下多选
        initialToolBar() // 初始化工具栏
        addObserver() // 注册需要监听的对象
    }

添加方法 addObserver()

    /// 注册需要监听的事件
    func addObserver() {
        // 监听设备旋转事件
        NotificationCenter.default.addObserver(
        self,
        selector: #selector(self.updateLayoutWhenOrientationChanged),
        name: NSNotification.Name.UIDeviceOrientationDidChange,
        object: nil)
    }

添加方法 updateLayoutWhenOrientationChanged()

    /// 设备旋转时重新布局
    @objc func updateLayoutWhenOrientationChanged() {
        setupToolBarFrame() // 对工具栏进行布局
    }

25.现在再看是不是很棒!我们这篇教程到这里就结束了,谢谢大家的耐心阅读!

14
14

后记:写这篇教程花了三天时间,我没有预计到居然这么漫长。其实当时解决问题写代码的时间很快,只要一个小时左右。写教程不像调试代码,它需要在逻辑上一气呵成,因此前后不断的更换截图、更新代码,而且示例虽然简单,但要尽量做到合理封装、逻辑清晰,在代码规范上也是一个必要的示范。我还会继续坚持写教程,相信以后会越写越快、越写越清晰。大家有什么建议随时提啊,我的邮箱: pmtnmd@163.com 。

欢迎访问 我的个人网站 ,阅读更多文章。


题图:The road to Mt Cook - Quentin Leclercq @unsplash