Quantcast
Viewing all articles
Browse latest Browse all 5930

Swift自定义UITabBar

前言

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

效果展示:

Image may be NSFW.
Clik here to view.
自定义UITabBar

从零开始

先说一下思路

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

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

Image may be NSFW.
Clik here to view.
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 的实际效果了,运行后,查看效果:

    Image may be NSFW.
    Clik here to view.
    基本的运行效果

    现在明显的问题就是我们的原始图片是红色的,为什么现在都是灰、蓝色,因为 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)
    }

    运行后,看到效果好像乱乱的,暂时不用在意,在后面的代码中会慢慢整理出理想的效果。

    Image may be NSFW.
    Clik here to view.
    乱糟糟的

    简单分析上面的代码:这里我在中间加入了三个自定义的按钮。这样的话,最下面应该是有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)
        }
    }

    看看运行结果:

    Image may be NSFW.
    Clik here to view.
    自定义按钮后

    首先,我们发现了乱的原因,就是自定义的按钮和原本的 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 ,那么很多数据就可以使用了。现在看看效果:

    Image may be NSFW.
    Clik here to view.
    修改自定义按钮后

  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 是下标值。看看这时候的效果吧:

    Image may be NSFW.
    Clik here to view.
    完成后

  7. 最后再说一点,有时候我们需要给自定义的 IWCustomTabBarView 添加背景图片,那么这时候会出现一个问题,就是原本的 TabBar 的浅灰色背景始终会有一条线,此时在 IWCustomTabBarController 文件的 viewDidLoad() 方法中添加下面的代码即可。

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

完了

作者:xxh0307 发表于2016/10/26 16:15:59 原文链接
阅读:20 评论:0 查看评论

Viewing all articles
Browse latest Browse all 5930

Trending Articles