1. 程式人生 > >Swift自定義UITabBar

Swift自定義UITabBar

前言

很多時候,系統原生的 UITabBar 並不能滿足我們的需求,譬如我們想要給圖示做動態的改變,或者比較炫一點的展示,原生的處理起來都很麻煩。所以很多時候都需要自定義一個 UITabBar,裡面的圖示、顏色、背景等等都可以根據需求去改變。

效果展示:

自定義UITabBar

從零開始

先說一下思路

頁面繼承自 UITabBarController ,然後自定義一個 UIView ,新增到 TabBar 上。取消原本的控制按鈕。建立自定義按鈕,即重寫 UIButtonimageView 、和 titleLabelframe ,完成圖片、文字的重新佈局。最後實現不同按鈕的協議方法。

效果圖中,只有兩邊的兩個頁面在 UITabBarController

的管理下,中間三個都是通過自定義按鈕實現的模態頁面,即 present 過去的。多用於拍攝圖片、錄製視訊、發表動態等功能。

Demo檔案

程式碼實現:

  1. 首先不妨先建立三個基礎檔案,然後在豐富程式碼。其中, IWCustomButton 繼承自 UIButtonIWCustomTabBarView 繼承自 UIViewIWCustomTabBarController 繼承自 UITabBarController

  2. 修改 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 }
  3. 首先在 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 的實際效果了,執行後,檢視效果:

    基本的執行效果

    現在明顯的問題就是我們的原始圖片是紅色的,為什麼現在都是灰、藍色,因為 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]
        }

    執行後便可檢視到原始的圖片效果。

  4. 編寫檔案 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 ,而這個屬性就能取代遍歷。

    其實存在的問題也很明顯,就是這麼寫的話很難去擴充套件,譬如如果上面的程式碼已經完成了,但是臨時需要減少一個自定義按鈕,那麼就需要改動多個地方。這裡只是提供一種自定義的思路,只是說還有很多可以優化的地方。

  5. 關於自定義的 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 ,那麼很多資料就可以使用了。現在看看效果:

    修改自定義按鈕後

  6. 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 是下標值。看看這時候的效果吧:

    完成後

  7. 最後再說一點,有時候我們需要給自定義的 IWCustomTabBarView 新增背景圖片,那麼這時候會出現一個問題,就是原本的 TabBar 的淺灰色背景始終會有一條線,此時在 IWCustomTabBarController 檔案的 viewDidLoad() 方法中新增下面的程式碼即可。

        //  去除 TabBar 陰影
        let originalTabBar = UITabBar.appearance()
        originalTabBar.shadowImage = UIImage()
        originalTabBar.backgroundImage = UIImage()

完了