Search code examples
iosswiftuitabbarcontrolleruitabbar

Custom UITabBar irregular shape programmatically


I need to create custom tabBar irregular shape programmatically. I found a lot of decisions, but they all are connected to Interface Builder. The code is below. All the methods of customized tabBar don't call while debugging.

final class TabBar: UITabBarController {

    var customTabBar = CustomizedTabBar()
    override var tabBar: UITabBar {
        return customTabBar
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .naviBarBlack
        UITabBar.appearance().barTintColor = .naviBarBlack
        UITabBar.appearance().clipsToBounds = false
        tabBar.tintColor = .white
        tabBar.itemPositioning = .centered
        
        setupVCs()
    }
    
    func setupVCs() {
        guard let homeUnselected = UIImage(named: "home-unselected"),
        let homeSelected = UIImage(named: "home-selected"),
        let likeUnselected = UIImage(named: "like-unselected"),
        let likeSelected = UIImage(named:"like-selected") else {return}
        
        self.viewControllers = [
            
            createNavController(for: MainScreenViewController(),
                                image: homeUnselected,
                                selected: homeSelected),
            createNavController(for: UIViewController(),
                                image: likeUnselected,
                                selected: likeSelected)
        ]
    }
    
    private func createNavController(for rootViewController: UIViewController,
                                     image: UIImage,
                                     selected: UIImage) -> UIViewController {
        let navController = UINavigationController(rootViewController: rootViewController)
        navController.tabBarItem.image = image
        navController.tabBarItem.selectedImage = selected
        return navController
    }
}

class CustomizedTabBar: UITabBar {

    private var shapeLayer: CALayer?

    private func addShape() {
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = createPath()
        shapeLayer.strokeColor = UIColor.lightGray.cgColor
        shapeLayer.fillColor = UIColor.white.cgColor
        shapeLayer.lineWidth = 1.0

        if let oldShapeLayer = self.shapeLayer {
            self.layer.replaceSublayer(oldShapeLayer, with: shapeLayer)
        } else {
            self.layer.insertSublayer(shapeLayer, at: 0)
        }

        self.shapeLayer = shapeLayer
    }

    override func draw(_ rect: CGRect) {
        self.addShape()
    }

    func createPath() -> CGPath {

        let height: CGFloat = 37.0
        let path = UIBezierPath()
        let centerWidth = self.frame.width / 2

        path.move(to: CGPoint(x: 0, y: 0)) // start top left
        path.addLine(to: CGPoint(x: (centerWidth - height * 2), y: 0)) // the beginning of the trough

        // first curve down
        path.addCurve(to: CGPoint(x: centerWidth, y: height),
                      controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
        // second curve up
        path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
                      controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))

        // complete the rect
        path.addLine(to: CGPoint(x: self.frame.width, y: 0))
        path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
        path.addLine(to: CGPoint(x: 0, y: self.frame.height))
        path.close()

        return path.cgPath
    }

    override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
        let buttonRadius: CGFloat = 35
        return abs(self.center.x - point.x) > buttonRadius || abs(point.y) > buttonRadius
    }

    func createPathCircle() -> CGPath {

        let radius: CGFloat = 37.0
        let path = UIBezierPath()
        let centerWidth = self.frame.width / 2

        path.move(to: CGPoint(x: 0, y: 0))
        path.addLine(to: CGPoint(x: (centerWidth - radius * 2), y: 0))
        path.addArc(withCenter: CGPoint(x: centerWidth, y: 0), radius: radius, startAngle: CGFloat(180).degreesToRadians, endAngle: CGFloat(0).degreesToRadians, clockwise: false)
        path.addLine(to: CGPoint(x: self.frame.width, y: 0))
        path.addLine(to: CGPoint(x: self.frame.width, y: self.frame.height))
        path.addLine(to: CGPoint(x: 0, y: self.frame.height))
        path.close()
        return path.cgPath
    }
}

extension CGFloat {
    var degreesToRadians: CGFloat { return self * .pi / 180 }
    var radiansToDegrees: CGFloat { return self * 180 / .pi }
}

So this's what i want to see (from https://betterprogramming.pub/draw-a-custom-ios-tabbar-shape-27d298a7f4fa)

And what i get.


Solution

  • While the Apple docs for UITabBarController state:

    You should never attempt to manipulate the UITabBar object itself stored in this property.

    you can find many, many examples of custom tab bars out there.

    For your specific approach, don't try overriding var tabBar:

    Instead, if you have your TabBarController in Storyboard, assign the custom class of its TabBar to CustomizedTabBar.

    Or, if you're instantiating the Controller from code, you could try this:

    override func viewDidLoad() {
        super.viewDidLoad()
    
        let tabBar = { () -> CustomizedTabBar in
            let tabBar = CustomizedTabBar()
            tabBar.delegate = self
            return tabBar
        }()
        self.setValue(tabBar, forKey: "tabBar")
        
        // ... the rest of your viewDidLoad()
    }
    

    I'd recommend reading through several other examples though, and look for a common (reliable) approach.