如何在自定義 Tool Bar 和 Tab Bar 之間切換顯示

鏡畫者發表於2019-03-04

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

相關文章