Swift自定義UITabBar

前言

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

效果展示:

自定義UITabBar

從零開始

先說一下思路

頁(yè)面繼承自 UITabBarController ,然后自定義一個(gè) UIView ,添加到 TabBar 上。取消原本的控制按鈕。創(chuàng)建自定義按鈕,即重寫 UIButtonimageView 、和 titleLabelframe ,完成圖片、文字的重新布局。最后實(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)圖片名:

Demo文件

代碼實(shí)現(xiàn):
  1. 首先不妨先建立三個(gè)基礎(chǔ)文件,然后在豐富代碼。其中, IWCustomButton 繼承自 UIButtonIWCustomTabBarView 繼承自 UIView , IWCustomTabBarController 繼承自 UITabBarController 。

  2. 修改 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
    }
  1. 首先在 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)行后,查看效果:

基本的運(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)行后便可查看到原始的圖片效果。

  1. 編寫文件 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)化的地方。

  1. 關(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)在看看效果:

修改自定義按鈕后
  1. 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í)候的效果吧:

完成后
  1. 最后再說一點(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()

完了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • TabBarController是項(xiàng)目主要框架結(jié)構(gòu), 一般來說系統(tǒng)自帶的TabBarController已足夠使用...
    wolf_吳郎閱讀 3,312評(píng)論 0 2
  • 重要:這是針對(duì)于正在開發(fā)中的API或技術(shù)的預(yù)備文檔(預(yù)發(fā)布版本)。蘋果提供這份文檔的目的是幫助你按照文中描述的方式...
    金_波閱讀 2,132評(píng)論 3 13
  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,052評(píng)論 4 61
  • 新學(xué)期開始啦!我們班分到了一位和藹可親的新老師——楊育慧。 一開始,我們大家都以為楊老師是年過半...
    歡樂洋閱讀 468評(píng)論 0 0
  • 那年的倫敦不知道是什么樣的,傻子眼里只有光芒和神 穿透昏沉的霧的夜的流星是 跛子逃離的痕跡,軌跡 清晰可見到一點(diǎn)黑...
    pleasen閱讀 184評(píng)論 0 0

友情鏈接更多精彩內(nèi)容