Swift自定义UITabBar

前言

很多时候,系统原生的 UITabBar 并不能满足我们的需求,譬如我们想要给图标做动态的改变,或者比较炫一点的展示,原生的处理起来都很麻烦。所以很多时候都需要自定义一个 UITabBar,里面的图标、颜色、背景等等都可以根据需求去改变。

效果展示:

自定义UITabBar

从零开始

先说一下思路

页面继承自 UITabBarController ,然后自定义一个 UIView ,添加到 TabBar 上。取消原本的控制按钮。创建自定义按钮,即重写 UIButtonimageView 、和 titleLabelframe ,完成图片、文字的重新布局。最后实现不同按钮的协议方法。

效果图中,只有两边的两个页面在 UITabBarController 的管理下,中间三个都是通过自定义按钮实现的模态页面,即 present 过去的。多用于拍摄图片、录制视频、发表动态等功能。

简单地展示下 Demo 的文件,因为代码中会出现图片名:

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
    }
  1. 首先在 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]
        }

运行后便可查看到原始的图片效果。

  1. 编写文件 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 ,而这个属性就能取代遍历。

其实存在的问题也很明显,就是这么写的话很难去扩展,譬如如果上面的代码已经完成了,但是临时需要减少一个自定义按钮,那么就需要改动多个地方。这里只是提供一种自定义的思路,只是说还有很多可以优化的地方。

  1. 关于自定义的 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 ,那么很多数据就可以使用了。现在看看效果:

修改自定义按钮后
  1. 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 是下标值。看看这时候的效果吧:

完成后
  1. 最后再说一点,有时候我们需要给自定义的 IWCustomTabBarView 添加背景图片,那么这时候会出现一个问题,就是原本的 TabBar 的浅灰色背景始终会有一条线,此时在 IWCustomTabBarController 文件的 viewDidLoad() 方法中添加下面的代码即可。
        //  去除 TabBar 阴影
        let originalTabBar = UITabBar.appearance()
        originalTabBar.shadowImage = UIImage()
        originalTabBar.backgroundImage = UIImage()

完了

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339

推荐阅读更多精彩内容

  • TabBarController是项目主要框架结构, 一般来说系统自带的TabBarController已足够使用...
    wolf_吴郎阅读 3,134评论 0 2
  • 重要:这是针对于正在开发中的API或技术的预备文档(预发布版本)。苹果提供这份文档的目的是帮助你按照文中描述的方式...
    金_波阅读 2,004评论 3 13
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 11,977评论 4 60
  • 新学期开始啦!我们班分到了一位和蔼可亲的新老师——杨育慧。 一开始,我们大家都以为杨老师是年过半...
    欢乐洋阅读 342评论 0 0
  • 那年的伦敦不知道是什么样的,傻子眼里只有光芒和神 穿透昏沉的雾的夜的流星是 跛子逃离的痕迹,轨迹 清晰可见到一点黑...
    pleasen阅读 124评论 0 0