前言
很多时候,系统原生的 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]?) -> 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
的实际效果了,运行后,查看效果:现在明显的问题就是我们的原始图片是红色的,为什么现在都是灰、蓝色,因为
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()