怎样造一个垂直的 TarBarController? VerticalTabBar,一般用于 iPad

1,094 阅读4分钟

这种需求不多,遇到了,还是要处理的

思路就是容器控制器,ContainerViewController .

封装一个 VerticalTabBar ,做控制器的管理工作,

左侧还有一个按钮栏,可以用表视图,UITableView

每一个按钮,就是一个 Cell ,再把按钮的点击绑定到管理的对应控制器上,就完了

第一点,容器控制器的管理

就是存在多个控制器,用户点击 tabBar

切换控制器,当前的控制器,就是选中的控制器

  // 为了顺利初始化, set 的时候,为了避免重复操作,要做判断
   var _selectedIndex: Int = -1
    var selectedIndex: Int{
        get{
            return _selectedIndex
        }
        set(newValue){
            // 要做判断
            if newValue != selectedIndex, newValue < viewControllers.count{
                let selectedViewController = viewControllers[newValue]
               //  添加新的控制器
                addChild(selectedViewController)
                selectedViewController.view.frame = CGRect(x: tabBarWidth, y: 0, width: view.bounds.size.width - tabBarWidth, height: view.bounds.size.height)
                view.addSubview(selectedViewController.view)
                if selectedIndex >= 0{
                   // 移除旧的控制器
                    let previousViewController = viewControllers[selectedIndex]
                    previousViewController.view.removeFromSuperview()
                    previousViewController.removeFromParent()
                }
                _selectedIndex = newValue
                guard let items = tabBar.items else{
                    return
                }
                if selectedIndex < items.count{
                    tabBar.selectedItem = items[selectedIndex]
                }
               // 代理方法,切换控制器的时候,做点别的
                delegate?.tabBarController?(self, didSelect: selectedViewController)
            }
        }
    }

通过 selectedIndex 的 set 方法实现,选中一个按钮,就是指定一个控制器 selectedViewController, 添加新的控制器,移除旧的控制器

添加新的控制器,标准四步走

  • 调用 addChildViewController: ,告诉 UIKit , 你的容器控制器现在管理了你的子控制器
  • view.addSubview(selectedViewController.view) ,把子控制器的根视图,添加到容器控制器的视图层级上

记得布局,提供位置信息

  • 可以添加一些子控制器的根视图的布局约束
  • 调用子控制器的 didMoveToParentViewController: 方法

添加新的控制器,标准四步走

  • 调用子控制器的 willMoveToParentViewController: , 右边的值是 nil
  • 去除子控制器根视图的所有布局约束
  • previousViewController.view.removeFromSuperview(), 从容器控制器的根视图的视图层级中,去除子控制器的根视图
  • 调用 removeFromParentViewController, 来终结父子控制器的关系
想要在 Swift 的 set 方法中,同时改新值和旧值,需要一个辅助属性 _selectedIndex
初始化的 dummy

if newValue != selectedIndex, set 的时候要做判断,为了避免重复操作, 为了顺利初始化,var _selectedIndex: Int = -1, 这是一个不可能取到的值,一次使用有效

(喜欢玩算法的,都知道 dummy )

第二点,点击选项卡的回调,也就是代理方法

@objc protocol TabBarControllerDelegate: class {
    @objc optional
    func tabBarController(_ tabBarController: VerticalTabBarController, didSelect viewController: UIViewController)
    @objc optional
    func tabBarController(_ tabBarController: VerticalTabBarController, shouldSelect viewController: UIViewController) -> Bool
}

第一个代理方法,didSelect , 可以在切换控制器的时候,做点别的,埋个点 他的实现时机,就在上块代码的尾部

第二个代理方法,shouldSelect, 可以做一下判断,没登录不能进个人中心,要去登录

func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? {
        let new = viewControllers[indexPath.row]
        let result = delegate?.tabBarController?(self, shouldSelect: new)
        if let answer = result, answer{
            return indexPath
        }
        else{
            return tableView.indexPathForSelectedRow
        }
    }

他的实现时机,在 tableView 的代理回调中,

上一个方法的时候,也在 tableView 的代理回调中,通过 selectedIndexset 方法,绕了一下具体见源代码

Objective-C 的内省 -(BOOL) respondsToSelector: 判读实例是否有这样方法,

Swift 里面没有

delegate?.tabBarController?(self, didSelect: selectedViewController)

Swift 的可选类型,很强大,直接 optional 就可以了

本文代码 github.com/coyingcat/V…

本文基于 Objective-C 的 futuresimple/FSVerticalTabBarController.

第三点,花里胡哨的,绘制选中图像渐变

override func draw(_ rect: CGRect) {
        let context = UIGraphicsGetCurrentContext()
         // flip the coordinates system
        context?.translateBy(x: 0, y: bounds.height)
        context?.scaleBy(x: 1, y: -1)
        
        // draw an image in the center of the cell
        guard let imageSize = icon?.size else {
            return
        }
        
        let imageRect = CGRect(x: (bounds.size.width - imageSize.width)/2.0,  y: (bounds.size.height - imageSize.height)/2.0 + 15, width: imageSize.width, height: imageSize.height)
        // draw either a selection gradient/glow or a regular image
        guard isSelected else {
            if let iconImage = icon {
                UIImage(cgImage: iconImage.cgImage!, scale: iconImage.scale, orientation: UIImage.Orientation.down).withHorizontallyFlippedOrientation().draw(in: imageRect)
            }
            return
        }
        
        // setup shadow
        let shadowOffset = CGSize(width: 0, height: 1)
        let shadowBlur: CGFloat = 3
        let cgShadowColor = UIColor.black.cgColor
        // setup gradient
        let alphas: [CGFloat] = [0.8,0.6, 0, 0.1,0.5]
        let locations: [CGFloat] = [0, 0.55, 0.55, 0.7, 1]
        let components: [CGFloat] = [1, 1, 1, alphas[0], 1,
                                     1, 1, alphas[1], 1, 1,
                                     1, alphas[2], 1, 1,1,
                                     alphas[3],1, 1,1,alphas[4]]
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        guard let colorGradient = CGGradient(colorSpace: colorSpace, colorComponents: components, locations: locations, count: 5) else {
            return
        }
        // set shadow
        context?.setShadow(offset: shadowOffset, blur: shadowBlur, color: cgShadowColor)
        // set transparency layer and clip to mask
        context?.beginTransparencyLayer(auxiliaryInfo: nil)
        context?.clip(to: imageRect, mask: icon!.cgImage!)
        // fill and end the transparency layer
        context?.setFillColor(selectedImageTintColor.cgColor)
        context?.fill(imageRect)
        let start = CGPoint(x: imageRect.midX, y: imageRect.origin.y)
        let end = CGPoint(x: imageRect.midX - imageRect.height/4, y: imageRect.height + imageRect.minY)
        context?.drawLinearGradient(colorGradient, start: end, end: start, options: [])
        context?.endTransparencyLayer()
    }

因为 override func draw(_ rect: CGRect) { 方法中,自带了一个绘图上下文,先获取当前上下文,翻转坐标系,弄阴影,弄渐变

Objective-C 里面又一个这样的方法 CGContextDrawImage(context, imageRect,self.iconImage.CGImage);

Swift 里面没有这个,都是直接 iconImage.draw(in: imageRect).

Screen Shot 2019-08-25 at 11.57.23 PM.png

绘制原图有坑,坐标系原因,

怎样做一个翻转,我是先旋转 180 度,再做一个左右翻转

UIImage(cgImage: iconImage.cgImage!, scale: iconImage.scale, orientation: UIImage.Orientation.down).withHorizontallyFlippedOrientation().draw(in: imageRect)