前言
很多時候,系統原生的 UITabBar
并不能滿足我們的需求,譬如我們想要給圖標做動態的改變,或者比較炫一點的展示,原生的處理起來都很麻煩。所以很多時候都需要自定義一個 UITabBar
,里面的圖標、顏色、背景等等都可以根據需求去改變。
效果展示:
從零開始
先說一下思路
頁面繼承自 UITabBarController
,然后自定義一個 UIView
,添加到 TabBar
上。取消原本的控制按鈕。創建自定義按鈕,即重寫 UIButton
的 imageView
、和 titleLabel
的 frame
,完成圖片、文字的重新布局。最后實現不同按鈕的協議方法。
效果圖中,只有兩邊的兩個頁面在 UITabBarController
的管理下,中間三個都是通過自定義按鈕實現的模態頁面,即 present
過去的。多用于拍攝圖片、錄制視頻、發表動態等功能。
簡單地展示下 Demo
的文件,因為代碼中會出現圖片名:
代碼實現:
首先不妨先建立三個基礎文件,然后在豐富代碼。其中,
IWCustomButton
繼承自UIButton
,IWCustomTabBarView
繼承自UIView
,IWCustomTabBarController
繼承自UITabBarController
。修改
AppDelegate
文件中didFinishLaunchingWithOptions
方法,保證啟動時沒有異常:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// 創建Window
window = UIWindow(frame: UIScreen.main.bounds)
// 初始化一個tabbar
let customTabBar = IWCustomTabBarController()
// 設置根控制器
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 = ["首頁","我的"]
// MARK: - LifeCycle
override func viewDidLoad() {
super.viewDidLoad()
// 自定義 TabBar 外觀
createCustomTabBar(addHeight: 0)
// 創建子控制器
addDefaultChildViewControllers()
// 設置每一個子頁面的按鈕展示
setChildViewControllerItem()
}
// MARK: - Private Methods
/// 添加默認的頁面
fileprivate func addDefaultChildViewControllers() {
let vc1 = UIViewController()
vc1.view.backgroundColor = UIColor.white
let vc2 = UIViewController()
vc2.view.backgroundColor = UIColor.lightGray
viewControllers = [vc1, vc2]
}
/// 設置外觀
///
/// - parameter addHeight: 增加高度,0 為默認
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)
}
/// 設置子頁面的item項
fileprivate func setChildViewControllerItem() {
guard let containViewControllers = viewControllers else {
print("?? 設置子頁面 item 項失敗 ??")
return
}
if containViewControllers.count != tabBarImageNames.count {
fatalError("子頁面數量和設置的tabBarItem數量不一致,請檢查!!")
}
// 遍歷子頁面
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]
}
}
}
上面就是一個基本的純代碼創建的 UITabBarController
的實際效果了,運行后,查看效果:
簡單說一下上面的代碼:
createCustomTabBar(_: )
方法傳入的參數主要是為了控制高度,嘗試改變參數值可看到效果。 addDefaultChildViewControllers()
方法是添加子頁面,這里需要說的是并沒有在這個方法里設置文字和圖片,而是選擇重新創建一個方法 setChildViewControllerItem()
,因為后面我們需要獲取頁面有幾個 UITabBarItem
。現在明顯的問題就是我們的原始圖片是紅色的,為什么現在都是灰、藍色,因為
UITabBar
使用圖片時渲染了,如果我們需要使用原始圖片,則對 UIImage
方法擴展:
extension UIImage {
var originalImage: UIImage {
return self.withRenderingMode(.alwaysOriginal)
}
}
然后修改遍歷子頁面的代碼:
// 遍歷子頁面
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]
}
運行后便可查看到原始的圖片效果。
- 編寫文件
IWCustomTabBarView
:
import UIKit
// 自定義按鈕功能
enum IWCustomButtonOperation {
case customRecordingVideo // 錄像
case customTakePhoto // 拍照
case customMakeTape // 錄音
}
/// 頁面按鈕點擊協議
protocol IWCustomTabBarViewDelegate {
/// 點擊tabBar 管理下的按鈕
///
/// - parameter customTabBarView: 當前視圖
/// - parameter didSelectedButtonTag: 點擊tag,這個是區分標識
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedButtonTag: Int)
/// 點擊自定義的純按鈕
///
/// - parameter customTabBarView: 當前視圖
/// - parameter didSelectedOpertaionButtonType: 按鈕類型,拍照、攝像、錄音
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedOpertaionButtonType: IWCustomButtonOperation)
}
class IWCustomTabBarView: UIView {
// MARK: - Properties
// 協議
var delegate: IWCustomTabBarViewDelegate?
// 操作按鈕數組
fileprivate var operationButtons = [IWCustomButton]()
// tabbar 管理的按鈕數組
fileprivate var customButtons = [IWCustomButton]()
// 自定義按鈕圖片、標題
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 頁面 init(coder:) 方法沒有實現")
}
/// 布局控件
override func layoutSubviews() {
super.layoutSubviews()
// 設置位置
let btnY: CGFloat = 0
let btnWidth = bounds.width / CGFloat(subviews.count)
let btnHeight = bounds.height
// 這里其實就兩個
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
}
}
// 這里有三個
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
/// 根據原始的 TabBarItem 設置自定義Button
///
/// - parameter originalTabBarItem: 原始數據
func addCustomTabBarButton(by originalTabBarItem: UITabBarItem) {
// 添加初始按鈕
let customButton = IWCustomButton()
customButtons.append(customButton)
addSubview(customButton)
// 添加點擊事件
customButton.addTarget(self, action: #selector(customButtonClickedAction(customBtn:)), for: .touchUpInside)
// 默認展示第一個頁面
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)
}
}
/// 操作按鈕點擊事件
@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
}
}
// 保證按鈕的狀態正常顯示
fileprivate var lastCustomButton = IWCustomButton()
/// tabbar 管理下按鈕的點擊事件
@objc fileprivate func customButtonClickedAction(customBtn: IWCustomButton) {
delegate?.iwCustomTabBarView(customTabBarView: self, customBtn.tag)
lastCustomButton.isSelected = false
customBtn.isSelected = true
lastCustomButton = customBtn
}
}
在 IWCustomTabBarController
文件的 setChildViewControllerItem()
方法中,修改遍歷子頁面的代碼,獲取當前的 UITabBarItem
:
// 遍歷子頁面 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]
// 添加相對應的自定義按鈕
customTabBarView.addCustomTabBarButton(by: singleVC.tabBarItem)
}
運行后,看到效果好像亂亂的,暫時不用在意,在后面的代碼中會慢慢整理出理想的效果。
簡單分析上面的代碼:這里我在中間加入了三個自定義的按鈕。這樣的話,最下面應該是有5個按鈕的。當然也可以加入一個或者兩個等,只需要修改上面對應的數值就可以了。這里面比較主要的就是自定義協議 IWCustomTabBarViewDelegate
和布局方法 layoutSubviews
,布局方法里如果能理解兩個 for
循環和對應數組中的數據來源、作用,那么問題就簡單很多了。
這里要說一個屬性 lastCustomButton
,這個屬性會讓我們避免不必要的遍歷按鈕,有些時候多個按鈕只能有一個被選中時,有種常見的方法就是遍歷按鈕數組,令其中一個 isSelected = true
,其他按鈕的 isSelected = false
,而這個屬性就能取代遍歷。
其實存在的問題也很明顯,就是這么寫的話很難去擴展,譬如如果上面的代碼已經完成了,但是臨時需要減少一個自定義按鈕,那么就需要改動多個地方。這里只是提供一種自定義的思路,只是說還有很多可以優化的地方。
- 關于自定義的
UIButotn
,是個很有意思的地方。因為視覺上的改變都是在這里發生,先使用默認的設置:
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:) 方法沒有實現")
}
/// 根據傳入的 UITabBarItem 設置數據顯示
///
/// - parameter tabBarItem: 數據來源
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
/// 根據原始的 TabBarItem 設置自定義Button
///
/// - parameter originalTabBarItem: 原始數據
func addCustomTabBarButton(by originalTabBarItem: UITabBarItem) {
// 添加初始按鈕
let customButton = IWCustomButton()
customButtons.append(customButton)
addSubview(customButton)
// 傳值
customButton.setTabBarItem(tabBarItem: originalTabBarItem)
// 添加點擊事件
customButton.addTarget(self, action: #selector(customButtonClickedAction(customBtn:)), for: .touchUpInside)
// 默認展示第一個頁面
if customButtons.count == 1 {
customButtonClickedAction(customBtn: customButton)
}
}
看看運行結果:
首先,我們發現了亂的原因,就是自定義的按鈕和原本的 UITabBarItem
的顯示起了沖突。那么先修改這個問題:在 IWCustomTabBarController
方法中頁面即將出現時添加方法:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
// 移除原生的 TabBarItem ,否則會出現覆蓋現象
tabBar.subviews.forEach { (subView) in
if subView is UIControl {
subView.removeFromSuperview()
}
}
}
那么上面重復顯示的原生項此時就移除了。下一個問題:發現自定義按鈕圖像的大小不一致。其實中間圖片本身的大小就是比兩邊的大的。以 2x.png 為例,中間的圖標是 70x70,而兩邊的是 48x48。如果在沒有文字顯示的情況下,在按鈕的初始化方法中添加 imageView?.contentMode = .center
,圖片居中展示,自定義按鈕到這個地方就可以結束了(可以嘗試不要 title
,查看運行效果)。甚至可以在自定義按鈕的初始化方法里使用仿射變換來放大、縮小圖片。
這里為了控制圖片、文字的位置,重寫 UIButton
的兩個方法:
/// 重寫 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)
}
對上面代碼做簡單地說明,首先說方法中 contentRect
這個變量,它的 size
是這個 UIButton
的大小,而不是單獨的 UIImageView
,或者 titleLabel
的大小。上面的一些具體數值,譬如 4 / 9
等這種奇葩的比例數值,僅僅是我根據自己的審美觀隨便寫入的一些數值,至于到具體的開發中,可以固定大小,也可以使用更加細致的比例,因為 tabBar
默認的高度是 49 ,那么很多數據就可以使用了。現在看看效果:
- 在
IWCustomTabBarController
文件中實現IWCustomTabBarView
文件中的協議方法,首先添加協議,然后實現方法,別忘了令customTabBarView.delegate = self
:
// MARK: - IWCustomTabBarViewDelegate
/// 點擊 tabbar 管理下的按鈕
func iwCustomTabBarView(customTabBarView: IWCustomTabBarView, _ didSelectedButtonTag: Int) {
selectedIndex = didSelectedButtonTag
}
/// 點擊自定義添加的的按鈕
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)
}
上面的代碼,只單獨說一點,就是協議方法 iwCustomTabBarView(customTabBarView : , _ didSelectedButtonTag)
中, selectedIndex
這個屬性并非我們自己定義的變量,而是系統設置的,所以這時候 didSelectedButtonTag
所代表值就顯得很有意思了,它正是我們在 UITabBar
管理下 ViewController
是下標值。看看這時候的效果吧:
- 最后再說一點,有時候我們需要給自定義的
IWCustomTabBarView
添加背景圖片,那么這時候會出現一個問題,就是原本的TabBar
的淺灰色背景始終會有一條線,此時在IWCustomTabBarController
文件的viewDidLoad()
方法中添加下面的代碼即可。
// 去除 TabBar 陰影
let originalTabBar = UITabBar.appearance()
originalTabBar.shadowImage = UIImage()
originalTabBar.backgroundImage = UIImage()