Swift自定義UITabBar
前言
很多時候,系統原生的 UITabBar
並不能滿足我們的需求,譬如我們想要給圖示做動態的改變,或者比較炫一點的展示,原生的處理起來都很麻煩。所以很多時候都需要自定義一個 UITabBar
,裡面的圖示、顏色、背景等等都可以根據需求去改變。
效果展示:
從零開始
先說一下思路
頁面繼承自 UITabBarController
,然後自定義一個 UIView
,新增到 TabBar
上。取消原本的控制按鈕。建立自定義按鈕,即重寫 UIButton
的 imageView
、和 titleLabel
的 frame
,完成圖片、文字的重新佈局。最後實現不同按鈕的協議方法。
效果圖中,只有兩邊的兩個頁面在 UITabBarController
present
過去的。多用於拍攝圖片、錄製視訊、發表動態等功能。
程式碼實現:
首先不妨先建立三個基礎檔案,然後在豐富程式碼。其中,
IWCustomButton
繼承自UIButton
,IWCustomTabBarView
繼承自UIView
,IWCustomTabBarController
繼承自UITabBarController
。修改
AppDelegate
檔案中didFinishLaunchingWithOptions
方法,保證啟動時沒有異常:func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?)
首先在
IWCustomTabBarController
檔案中新增程式碼:// IWCustomTabBarController.swift
上面就是一個基本的純程式碼建立的
UITabBarController
的實際效果了,執行後,檢視效果:現在明顯的問題就是我們的原始圖片是紅色的,為什麼現在都是灰、藍色,因為
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()