#26 custom TabBar|客製化TabBar

In UIKit

Ethan
彼得潘的 Swift iOS / Flutter App 開發教室
12 min readMay 27, 2024

--

先上完成圖:

說明概念:

設計一個TabBar,例如我有四個分頁,我點選按鈕顯示我指定的分頁。
TabBar怎麼設計?
其實方法有很多種,就看你想要什麼樣的樣式。
有些人想要的樣式是想要有特別曲線的可以用path.addline去實現:

但我想要的TabBar是不要粘在底部、想要他離底部有點距離的的(•̆ꈊ•̆ )(•̆ꈊ•̆ )

正文開始:

原理我們在設計的View Controller新增兩個view:
1. 一個是ContentView
2. 一個是TabBarView

第一個用途是把Contnet View 拉一個@IBOutlet,創建其他的
Other View Controller 可以藉此顯示在View Controller上。
第二個用途就是自定義外觀啦~

創建ContentView:

新增一個UIView到 View Controller,並且將其設置AutoLayout to SuperView。

再加入一個UIView作為TabBar View,設置AutoLayout:
(Leading, Trailing, Bottom)

可以依自己喜好調整,我把bar的高度設成60,其他的參數如下:

接著拉IBOutlet:

TabBar旁邊我想做成半圓的,所以如程式碼我把高度除2:

加入TabBar的按鈕:
我這裡的做法是我會使用Stack View來排版,所以先設置一個UIView並調整他的內容。

加入UIView到TabBar裡,並且在UIView裡面加入圖片及按鈕。
再來將他embed in 一個Stack View。

Stack View:
Axis設定Horizontal
Distribution設定 Fill Equally
Spacing = 0

我們的Stack View跟TabBarView是一樣大的,所以把他設定layout:

我們按鈕跟View一樣大,所以把他設定layout:

圖片的話看個人,反正他只是顯示而已,我直接使其在View的正中間:

當你完成上述後會報錯,因為你還沒加入圖片,圖片有加入報錯就會消失了

記得,我們按鍵不用文字,所以把文字刪除:

完成一個設定後,看你的TabBar有幾頁,就複製幾個出來,並且幫他重新命名:

記住一件事情,button一定要在圖片的下面,不然你做完後畫面還是不能切換:

第一次執行:
發現圓角不見了。

加入以下切除即可:

按鈕設置Action:

四個按鈕同一個Action:

按鈕設置Tag:
接下來要透過程式去執行,我點到哪一個按鈕了。
點選按鈕後在Attributes Inspector的View裡面設置Tag,
四個按鈕依序設置1, 2, 3, 4。

我們在IBAction加入以下代碼來測試:

let tag = sender.tag
print(tag)

加入View Controller:
你做幾個Tabbar,就加入幾個。

用Cocoa Touch Class創立四個file,我分別取名first, second, third, fourth來對照:

為新增的View Controller設置Class & Identity:

程式設定:
首先ViewDidLoad要先出現首頁,因此先寫一個func來呼叫首頁。
這次我們要使用 instantiateViewController 把ViewController實例化

我們從storyboard呼叫他,如下圖,然後我們發現他是一個optional:

因此使用guard let:
記得要轉型!

func firstPage() {
guard let firstPage = self.storyboard?.instantiateViewController(identifier: "FirstViewController") as? FirstViewController else { return }
}

當我們把View Controller 實例化之後,需要使其顯示於ContentView,所以我們這時候要拉IBOutlet:

addChild(_:):
可以參考連結

它是用來將一個子視圖添加到當前的視圖中。
就是把四個ViewController添加到ContentView的一個方法,
使用這個方法需要遵從生命週期:

1. 調用 addChild(_:):將子視圖控制器添加到父視圖控制器中。

2. 設置子視圖控制器的視圖框架:設置子視圖控制器視圖的大小和位置。

3. 將子視圖控制器的視圖添加到父視圖中:將子視圖控制器的視圖添加到父視圖中。

4. 調用 didMove(toParent:):通知子視圖控制器它已經被添加到父視圖控制器中。

    func firstPage() {
guard let firstPage = self.storyboard?.instantiateViewController(identifier: "FirstViewController") as? FirstViewController else { return }
//調用 addChild(_:)
self.addChild(firstPage)
//設置子視圖控制器的視圖框架
firstPage.view.frame = contentView.bounds
//將子視圖控制器的視圖添加到父視圖中
contentView.addSubview(firstPage.view)
//調用 didMove(toParent:)
firstPage.didMove(toParent: self)
}

接下來我們就可以為這幾個Button設置切換ViewController:

RemoveCurrentChildViewController:

仔細想想,我們如果成功呼叫ViewController,那呼叫出來後他會消失嗎?
並不會,所以我們一直點選頁面切換,他會一直累加上去,直到記憶體爆掉!
因此需要寫一個func,當我們點選進入到其他ViewController把進入原本的移除。

提外:
如果今天是有從網路抓取資料,資料是不是要再重新抓取呢?
我的想法是把抓取的資料先存在一個static 的 property。

整理code:
發現addChild生命週期有重複出現,因此重新優化整理。

import UIKit

class ViewController: UIViewController {

@IBOutlet weak var tabBarView: UIView!

@IBOutlet weak var contentView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
customizeTabBar()
firstPage()
}

func customizeTabBar () {
tabBarView.layer.cornerRadius = tabBarView.frame.height / 2
tabBarView.clipsToBounds = true
}
func firstPage() {
guard let firstPage = self.storyboard?.instantiateViewController(identifier: "FirstViewController") as? FirstViewController else { return }
self.addChild(firstPage)
firstPage.view.frame = contentView.bounds
contentView.addSubview(firstPage.view)
firstPage.didMove(toParent: self)
}

func removeCurrentChildViewController() {
for child in children {
child.willMove(toParent: nil)
child.view.removeFromSuperview()
child.removeFromParent()
}
}
func addChildViewController(withIdentifier identifier: String) {
guard let childViewController = self.storyboard?.instantiateViewController(identifier: identifier) as? UIViewController else { return }
self.addChild(childViewController)
childViewController.view.frame = contentView.bounds
contentView.addSubview(childViewController.view)
childViewController.didMove(toParent: self)
}
@IBAction func tapToChangeViews(_ sender: UIButton) {
let tag = sender.tag
removeCurrentChildViewController()
print(tag)
switch tag {
case 1:
firstPage()
case 2:
addChildViewController(withIdentifier: "SecondViewController")
case 3:
addChildViewController(withIdentifier: "ThirdViewController")
case 4:
addChildViewController(withIdentifier: "FourthViewController")
default:
break
}
}
}

問題:

使用SE裝置執行出現下圖

問題可能出在 customizeTabBarfirstPage 的執行順序或佈局過程中。當 viewDidLoad 方法中直接呼叫 firstPage() 時,視圖可能還沒有完成佈局造成,所以最後一步幫firstPage() 加上DispatchQueue:

大功告成~

Here’s my GitHub repository link:

--

--