前言
很多時(shí)候,系統(tǒng)原生的 UITabBar 并不能滿足我們的需求,譬如我們想要給圖標(biāo)做動(dòng)態(tài)的改變,或者比較炫一點(diǎn)的展示,原生的處理起來都很麻煩。所以很多時(shí)候都需要自定義一個(gè) UITabBar,里面的圖標(biāo)、顏色、背景等等都可以根據(jù)需求去改變。
效果展示:

從零開始
先說一下思路
頁(yè)面繼承自 UITabBarController ,然后自定義一個(gè) UIView ,添加到 TabBar 上。取消原本的控制按鈕。創(chuàng)建自定義按鈕,即重寫 UIButton 的 imageView 、和 titleLabel 的 frame ,完成圖片、文字的重新布局。最后實(shí)現(xiàn)不同按鈕的協(xié)議方法。
效果圖中,只有兩邊的兩個(gè)頁(yè)面在 UITabBarController 的管理下,中間三個(gè)都是通過自定義按鈕實(shí)現(xiàn)的模態(tài)頁(yè)面,即 present 過去的。多用于拍攝圖片、錄制視頻、發(fā)表動(dòng)態(tài)等功能。
簡(jiǎn)單地展示下 Demo 的文件,因?yàn)榇a中會(huì)出現(xiàn)圖片名:

代碼實(shí)現(xiàn):
首先不妨先建立三個(gè)基礎(chǔ)文件,然后在豐富代碼。其中,
IWCustomButton繼承自UIButton,IWCustomTabBarView繼承自UIView,IWCustomTabBarController繼承自UITabBarController。修改
AppDelegate文件中didFinishLaunchingWithOptions方法,保證啟動(dòng)時(shí)沒有異常:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// 創(chuàng)建Window
window = UIWindow(frame: UIScreen.main.bounds)
// 初始化一個(gè)tabbar
let customTabBar = IWCustomTabBarController()
// 設(shè)置根控制器
window?.rootViewController = customTabBar
window?.makeKeyAndVisible()
return true
}
- 首先在
IWCustomTabBarController文件中添加代碼:
// IWCustomTabBarController.swift
import UIKit
class IWCustomTabBarController: UITabBarController {
// MARK: - Properties
// 圖片
fileprivate let tabBarImageNames = ["tb_home","tb_person"]
fileprivate let tabBarTitles = ["首頁(yè)","我的"]
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
// 自定義 TabBar 外觀
createCustomTabBar(addHeight: 0)
// 創(chuàng)建子控制器
addDefaultChildViewControllers()
// 設(shè)置每一個(gè)子頁(yè)面的按鈕展示
setChildViewControllerItem()
}
// MARK: - Private Methods
/// 添加默認(rèn)的頁(yè)面
fileprivate func addDefaultChildViewControllers() {
let vc1 = UIViewController()
vc1.view.backgroundColor = UIColor.white
let vc2 = UIViewController()
vc2.view.backgroundColor = UIColor.lightGray
viewControllers = [vc1, vc2]
}
/// 設(shè)置外觀
///
/// - parameter addHeight: 增加高度,0 為默認(rèn)
fileprivate let customTabBarView = IWCustomTabBarView()
fileprivate func createCustomTabBar(addHeight: CGFloat) {
// 改變tabbar 大小
var oriTabBarFrame = tabBar.frame
oriTabBarFrame.origin.y -= addHeight
oriTabBarFrame.size.height += addHeight
tabBar.frame = oriTabBarFrame
customTabBarView.frame = tabBar.bounds
customTabBarView.frame.origin.y -= addHeight
customTabBarView.backgroundColor = UIColor.groupTableViewBackground
customTabBarView.frame.size.height = tabBar.frame.size.height + addHeight
customTabBarView.isUserInteractionEnabled = true
tabBar.addSubview(customTabBarView)
}
/// 設(shè)置子頁(yè)面的item項(xiàng)
fileprivate func setChildViewControllerItem() {
guard let containViewControllers = viewControllers else {
print("?? 設(shè)置子頁(yè)面 item 項(xiàng)失敗 ??")
return
}
if containViewControllers.count != tabBarImageNames.count {
fatalError("子頁(yè)面數(shù)量和設(shè)置的tabBarItem數(shù)量不一致,請(qǐng)檢查??!")
}
// 遍歷子頁(yè)面
for (index, singleVC) in containViewControllers.enumerated() {
singleVC.tabBarItem.image = UIImage(named: tabBarImageNames[index])
singleVC.tabBarItem.selectedImage = UIImage(named: tabBarImageNames[index] + "_selected")
singleVC.tabBarItem.title = tabBarTitles[index]
}
}
}
上面就是一個(gè)基本的純代碼創(chuàng)建的 UITabBarController 的實(shí)際效果了,運(yùn)行后,查看效果:

簡(jiǎn)單說一下上面的代碼:
createCustomTabBar(_: ) 方法傳入的參數(shù)主要是為了控制高度,嘗試改變參數(shù)值可看到效果。 addDefaultChildViewControllers() 方法是添加子頁(yè)面,這里需要說的是并沒有在這個(gè)方法里設(shè)置文字和圖片,而是選擇重新創(chuàng)建一個(gè)方法 setChildViewControllerItem() ,因?yàn)楹竺嫖覀冃枰@取頁(yè)面有幾個(gè) UITabBarItem 。現(xiàn)在明顯的問題就是我們的原始圖片是紅色的,為什么現(xiàn)在都是灰、藍(lán)色,因?yàn)?
UITabBar 使用圖片時(shí)渲染了,如果我們需要使用原始圖片,則對(duì) UIImage 方法擴(kuò)展:
extension UIImage {
var originalImage: UIImage {
return self.withRenderingMode(.alwaysOriginal)
}
}
然后修改遍歷子頁(yè)面的代碼:
// 遍歷子頁(yè)面
for (index, singleVC) in containViewControllers.enumerated() {
singleVC.tabBarItem.image = UIImage(named: tabBarImageNames[index]).originalImage
singleVC.tabBarItem.selectedImage = UIImage(named: tabBarImageNames[index] + "_selected").originalImage
singleVC.tabBarItem.title = tabBarTitles[index]
}
運(yùn)行后便可查看到原始的圖片效果。
- 編寫文件
IWCustomTabBarView:
import UIKit
// 自定義按鈕功能
enum IWCustomButtonOperation {
case customRecordingVideo // 錄像
case customTakePhoto // 拍照
case customMakeTape // 錄音
}
/// 頁(yè)面按鈕點(diǎn)擊協(xié)議
protocol IWCustomTabBarViewDelegate {
/// 點(diǎn)擊tabBar 管理下的按鈕
///
/// - parameter customTabBarView: 當(dāng)前視圖
/// - parameter didSelectedButtonTag: 點(diǎn)擊tag,這個(gè)是區(qū)分標(biāo)識(shí)
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedButtonTag: Int)
/// 點(diǎn)擊自定義的純按鈕
///
/// - parameter customTabBarView: 當(dāng)前視圖
/// - parameter didSelectedOpertaionButtonType: 按鈕類型,拍照、攝像、錄音
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedOpertaionButtonType: IWCustomButtonOperation)
}
class IWCustomTabBarView: UIView {
// MARK: - Properties
// 協(xié)議
var delegate: IWCustomTabBarViewDelegate?
// 操作按鈕數(shù)組
fileprivate var operationButtons = [IWCustomButton]()
// tabbar 管理的按鈕數(shù)組
fileprivate var customButtons = [IWCustomButton]()
// 自定義按鈕圖片、標(biāo)題
fileprivate let operationImageNames = ["tb_normol","tb_normol","tb_normol"]
fileprivate let operationTitls = ["攝像", "拍照", "錄音"]
// MARK: - Init
override init(frame: CGRect) {
super.init(frame: frame)
// 添加自定義按鈕
addOperationButtons()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("IWCustomTabBarView 頁(yè)面 init(coder:) 方法沒有實(shí)現(xiàn)")
}
/// 布局控件
override func layoutSubviews() {
super.layoutSubviews()
// 設(shè)置位置
let btnY: CGFloat = 0
let btnWidth = bounds.width / CGFloat(subviews.count)
let btnHeight = bounds.height
// 這里其實(shí)就兩個(gè)
for (index, customButton) in customButtons.enumerated() {
switch index {
case 0:
customButton.frame = CGRect(x: 0, y: 0, width: btnWidth, height: btnHeight)
customButton.tag = index
case 1:
customButton.frame = CGRect(x: btnWidth * 4, y: 0, width: btnWidth, height: btnHeight)
customButton.tag = index
default:
break
}
}
// 這里有三個(gè)
for (index, operBtn) in operationButtons.enumerated() {
let btnX = (CGFloat(index) + 1) * btnWidth
operBtn.frame = CGRect(x: btnX, y: btnY, width: btnWidth, height: btnHeight)
}
}
// MARK: - Public Methods
/// 根據(jù)原始的 TabBarItem 設(shè)置自定義Button
///
/// - parameter originalTabBarItem: 原始數(shù)據(jù)
func addCustomTabBarButton(by originalTabBarItem: UITabBarItem) {
// 添加初始按鈕
let customButton = IWCustomButton()
customButtons.append(customButton)
addSubview(customButton)
// 添加點(diǎn)擊事件
customButton.addTarget(self, action: #selector(customButtonClickedAction(customBtn:)), for: .touchUpInside)
// 默認(rèn)展示第一個(gè)頁(yè)面
if customButtons.count == 1 {
customButtonClickedAction(customBtn: customButton)
}
}
// MARK: - Private Methods
/// 添加操作按鈕
fileprivate func addOperationButtons() {
for index in 0 ..< 3 {
let operationBtn = IWCustomButton()
operationButtons.append(operationBtn)
operationBtn.setImage(UIImage(named: operationImageNames[index]), for: .normal)
operationBtn.setImage(UIImage(named: operationImageNames[index]), for: .highlighted)
operationBtn.setTitle(operationTitls[index], for: .normal)
operationBtn.tag = 100 + index
operationBtn.addTarget(self, action: #selector(operationButtonClickedAction(operBtn:)), for: .touchUpInside)
addSubview(operationBtn)
}
}
/// 操作按鈕點(diǎn)擊事件
@objc fileprivate func operationButtonClickedAction(operBtn: IWCustomButton) {
switch operBtn.tag {
case 100:
delegate?.iwCustomTabBarView(customTabBarView: self, .customRecordingVideo)
case 101:
delegate?.iwCustomTabBarView(customTabBarView: self, .customTakePhoto)
case 102:
delegate?.iwCustomTabBarView(customTabBarView: self, .customMakeTape)
default:
break
}
}
// 保證按鈕的狀態(tài)正常顯示
fileprivate var lastCustomButton = IWCustomButton()
/// tabbar 管理下按鈕的點(diǎn)擊事件
@objc fileprivate func customButtonClickedAction(customBtn: IWCustomButton) {
delegate?.iwCustomTabBarView(customTabBarView: self, customBtn.tag)
lastCustomButton.isSelected = false
customBtn.isSelected = true
lastCustomButton = customBtn
}
}
在 IWCustomTabBarController 文件的 setChildViewControllerItem() 方法中,修改遍歷子頁(yè)面的代碼,獲取當(dāng)前的 UITabBarItem :
// 遍歷子頁(yè)面 for (index, singleVC) in containViewControllers.enumerated() {
singleVC.tabBarItem.image = UIImage(named: tabBarImageNames[index])
singleVC.tabBarItem.selectedImage = UIImage(named: tabBarImageNames[index] + "_selected")
singleVC.tabBarItem.title = tabBarTitles[index]
// 添加相對(duì)應(yīng)的自定義按鈕
customTabBarView.addCustomTabBarButton(by: singleVC.tabBarItem)
}
運(yùn)行后,看到效果好像亂亂的,暫時(shí)不用在意,在后面的代碼中會(huì)慢慢整理出理想的效果。

簡(jiǎn)單分析上面的代碼:這里我在中間加入了三個(gè)自定義的按鈕。這樣的話,最下面應(yīng)該是有5個(gè)按鈕的。當(dāng)然也可以加入一個(gè)或者兩個(gè)等,只需要修改上面對(duì)應(yīng)的數(shù)值就可以了。這里面比較主要的就是自定義協(xié)議 IWCustomTabBarViewDelegate 和布局方法 layoutSubviews,布局方法里如果能理解兩個(gè) for 循環(huán)和對(duì)應(yīng)數(shù)組中的數(shù)據(jù)來源、作用,那么問題就簡(jiǎn)單很多了。
這里要說一個(gè)屬性 lastCustomButton ,這個(gè)屬性會(huì)讓我們避免不必要的遍歷按鈕,有些時(shí)候多個(gè)按鈕只能有一個(gè)被選中時(shí),有種常見的方法就是遍歷按鈕數(shù)組,令其中一個(gè) isSelected = true ,其他按鈕的 isSelected = false ,而這個(gè)屬性就能取代遍歷。
其實(shí)存在的問題也很明顯,就是這么寫的話很難去擴(kuò)展,譬如如果上面的代碼已經(jīng)完成了,但是臨時(shí)需要減少一個(gè)自定義按鈕,那么就需要改動(dòng)多個(gè)地方。這里只是提供一種自定義的思路,只是說還有很多可以優(yōu)化的地方。
- 關(guān)于自定義的
UIButotn,是個(gè)很有意思的地方。因?yàn)橐曈X上的改變都是在這里發(fā)生,先使用默認(rèn)的設(shè)置:
import UIKit
class IWCustomButton: UIButton {
override init(frame: CGRect) {
super.init(frame: frame)
titleLabel?.textAlignment = .center
setTitleColor(UIColor.gray, for: .normal)
setTitleColor(UIColor.red, for: .selected)
titleLabel?.font = UIFont.italicSystemFont(ofSize: 12)
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
print("?????? init(coder:) 方法沒有實(shí)現(xiàn)")
}
/// 根據(jù)傳入的 UITabBarItem 設(shè)置數(shù)據(jù)顯示
///
/// - parameter tabBarItem: 數(shù)據(jù)來源
func setTabBarItem(tabBarItem: UITabBarItem) {
setTitle(tabBarItem.title, for: .normal)
setImage(tabBarItem.image, for: .normal)
setImage(tabBarItem.selectedImage, for: .highlighted)
setImage(tabBarItem.selectedImage, for: .selected)
}
}
修改 IWCustomTabBarView 文件的 addCustomTabBarButton(by: ) 方法:
// MARK: - Public Methods
/// 根據(jù)原始的 TabBarItem 設(shè)置自定義Button
///
/// - parameter originalTabBarItem: 原始數(shù)據(jù)
func addCustomTabBarButton(by originalTabBarItem: UITabBarItem) {
// 添加初始按鈕
let customButton = IWCustomButton()
customButtons.append(customButton)
addSubview(customButton)
// 傳值
customButton.setTabBarItem(tabBarItem: originalTabBarItem)
// 添加點(diǎn)擊事件
customButton.addTarget(self, action: #selector(customButtonClickedAction(customBtn:)), for: .touchUpInside)
// 默認(rèn)展示第一個(gè)頁(yè)面
if customButtons.count == 1 {
customButtonClickedAction(customBtn: customButton)
}
}
看看運(yùn)行結(jié)果:

首先,我們發(fā)現(xiàn)了亂的原因,就是自定義的按鈕和原本的 UITabBarItem 的顯示起了沖突。那么先修改這個(gè)問題:在 IWCustomTabBarController 方法中頁(yè)面即將出現(xiàn)時(shí)添加方法:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 移除原生的 TabBarItem ,否則會(huì)出現(xiàn)覆蓋現(xiàn)象
tabBar.subviews.forEach { (subView) in
if subView is UIControl {
subView.removeFromSuperview()
}
}
}
那么上面重復(fù)顯示的原生項(xiàng)此時(shí)就移除了。下一個(gè)問題:發(fā)現(xiàn)自定義按鈕圖像的大小不一致。其實(shí)中間圖片本身的大小就是比兩邊的大的。以 2x.png 為例,中間的圖標(biāo)是 70x70,而兩邊的是 48x48。如果在沒有文字顯示的情況下,在按鈕的初始化方法中添加 imageView?.contentMode = .center ,圖片居中展示,自定義按鈕到這個(gè)地方就可以結(jié)束了(可以嘗試不要 title ,查看運(yùn)行效果)。甚至可以在自定義按鈕的初始化方法里使用仿射變換來放大、縮小圖片。
這里為了控制圖片、文字的位置,重寫 UIButton 的兩個(gè)方法:
/// 重寫 UIButton 的 UIImageView 位置
///
/// - parameter contentRect: 始位置
///
/// - returns: 修改后
override func imageRect(forContentRect contentRect: CGRect) -> CGRect {
let imageWidth = contentRect.size.height * 4 / 9
let imageHeight = contentRect.size.height
return CGRect(x: bounds.width / 2 - imageWidth / 2, y: imageHeight / 9, width: imageWidth, height: imageWidth)
}
/// 重寫 UIButton 的 TitleLabel 的位置
///
/// - parameter contentRect: 原始位置
///
/// - returns: 修改后
override func titleRect(forContentRect contentRect: CGRect) -> CGRect {
let titleWidth = contentRect.size.width
let titleHeight = contentRect.size.height / 3
return CGRect(x: bounds.width / 2 - titleWidth / 2, y: bounds.height - titleHeight, width: titleWidth, height: titleHeight)
}
對(duì)上面代碼做簡(jiǎn)單地說明,首先說方法中 contentRect 這個(gè)變量,它的 size 是這個(gè) UIButton 的大小,而不是單獨(dú)的 UIImageView ,或者 titleLabel 的大小。上面的一些具體數(shù)值,譬如 4 / 9 等這種奇葩的比例數(shù)值,僅僅是我根據(jù)自己的審美觀隨便寫入的一些數(shù)值,至于到具體的開發(fā)中,可以固定大小,也可以使用更加細(xì)致的比例,因?yàn)?tabBar 默認(rèn)的高度是 49 ,那么很多數(shù)據(jù)就可以使用了?,F(xiàn)在看看效果:

- 在
IWCustomTabBarController文件中實(shí)現(xiàn)IWCustomTabBarView文件中的協(xié)議方法,首先添加協(xié)議,然后實(shí)現(xiàn)方法,別忘了令customTabBarView.delegate = self:
// MARK: - IWCustomTabBarViewDelegate
/// 點(diǎn)擊 tabbar 管理下的按鈕
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedButtonTag: Int) {
selectedIndex = didSelectedButtonTag
}
/// 點(diǎn)擊自定義添加的的按鈕
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedOpertaionButtonType: IWCustomButtonOperation) {
switch didSelectedOpertaionButtonType {
case .customRecordingVideo:
print("攝像")
let vc = UIViewController()
vc.view.backgroundColor = UIColor.orange
addBackButton(on: vc.view)
present(vc, animated: true, completion: nil)
case .customTakePhoto:
print("拍照")
let vc = UIViewController()
vc.view.backgroundColor = UIColor.green
addBackButton(on: vc.view)
present(vc, animated: true, completion: nil)
case .customMakeTape:
print("錄音")
let vc = UIViewController()
vc.view.backgroundColor = UIColor.cyan
addBackButton(on: vc.view)
present(vc, animated: true, completion: nil)
}
}
fileprivate func addBackButton(on superView: UIView) {
let btn = UIButton()
btn.frame = CGRect(x: 100, y: 100, width: 100, height: 50)
btn.backgroundColor = UIColor.blue
btn.setTitle("返回", for: .normal)
btn.setTitleColor(UIColor.white, for: .normal)
btn.addTarget(self, action: #selector(dismissAction), for: .touchUpInside)
superView.addSubview(btn)
}
@objc func dismissAction() {
dismiss(animated: true, completion: nil)
}
上面的代碼,只單獨(dú)說一點(diǎn),就是協(xié)議方法 iwCustomTabBarView(customTabBarView : , _ didSelectedButtonTag) 中, selectedIndex 這個(gè)屬性并非我們自己定義的變量,而是系統(tǒng)設(shè)置的,所以這時(shí)候 didSelectedButtonTag 所代表值就顯得很有意思了,它正是我們?cè)?UITabBar 管理下 ViewController 是下標(biāo)值??纯催@時(shí)候的效果吧:

- 最后再說一點(diǎn),有時(shí)候我們需要給自定義的
IWCustomTabBarView添加背景圖片,那么這時(shí)候會(huì)出現(xiàn)一個(gè)問題,就是原本的TabBar的淺灰色背景始終會(huì)有一條線,此時(shí)在IWCustomTabBarController文件的viewDidLoad()方法中添加下面的代碼即可。
// 去除 TabBar 陰影
let originalTabBar = UITabBar.appearance()
originalTabBar.shadowImage = UIImage()
originalTabBar.backgroundImage = UIImage()