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

The road to Mt Cook

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

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

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

4

12

下面咱们从头开始建立示例工程:swift

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

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。效果以下:app

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。如今效果以下:ide

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

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

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

8

7

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

9

16.因此咱们打算自定义 Toolbar,且要知足如下几个特性:

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

是否是以为咱们要复刻 Tabbar 了?看起来还真有点像,不过咱们会作的稍微简单点,看完本文还有想法的能够再去打磨一下。

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

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

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.3三、带有背景色的空 Image View,用法是否是很特别。接着咱们在 UI 库中找到 Image View,放在 Visual Effect View 上层,并设置约束,高度的约束单独设置为0.33(高度直接在 View 尺寸中设置是不起做用的),其余约束相同。找到 Image ViewBackground 属性,设置为黑色加 30% 透明度。

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

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

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

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

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


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

相关文章
相关标签/搜索