6

I want to set corner radius and shadow for UITabBar, but I have a problem. This is my code

tabBar.barTintColor = .white
tabBar.isTranslucent = false

tabBar.layer.shadowOffset = CGSize(width: 0, height: 5)
tabBar.layer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 1).cgColor
tabBar.layer.shadowOpacity = 1;
tabBar.layer.shadowRadius = 25;

tabBar.layer.masksToBounds = false
tabBar.isTranslucent = true
tabBar.barStyle = .blackOpaque
tabBar.layer.cornerRadius = 13
tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

If i change tabBar.layer.masksToBounds = false to = true -> corner radius will be displayed , but shadow will not be.

Bhavesh Nayi
  • 705
  • 4
  • 15
Stormios
  • 127
  • 1
  • 8

3 Answers3

4

Swift 5

Try this. I achieved it by placing a custom view behind the tab bar. This works with Swift 5

import UIKit

class MainTabBarController:
    UITabBarController,
    UITabBarControllerDelegate {

    let customTabBarView: UIView = {

        let view = UIView(frame: .zero)

        view.backgroundColor = .white
        view.layer.cornerRadius = 20
        view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
        view.clipsToBounds = true

        view.layer.masksToBounds = false
        view.layer.shadowColor = UIColor.black.cgColor
        view.layer.shadowOffset = CGSize(width: 0, height: -8.0)
        view.layer.shadowOpacity = 0.12
        view.layer.shadowRadius = 10.0
        return view
    }()

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self

        addCustomTabBarView()
        hideTabBarBorder()
        setupTabBar()
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        customTabBarView.frame = tabBar.frame
    }

    override func viewDidAppear(_ animated: Bool) {
        var newSafeArea = UIEdgeInsets()

        newSafeArea.bottom += customTabBarView.bounds.size.height
        self.children.forEach({$0.additionalSafeAreaInsets = newSafeArea})
    }

    private func addCustomTabBarView() {
        customTabBarView.frame = tabBar.frame
        view.addSubview(customTabBarView)
        view.bringSubviewToFront(self.tabBar)
    }

    func hideTabBarBorder()  {
        let tabBar = self.tabBar
        tabBar.backgroundImage = UIImage.from(color: .clear)
        tabBar.shadowImage = UIImage()
        tabBar.clipsToBounds = true
    }

    func setupTabBar() {
        self.setViewControllers([tab1, tab2, tab3], animated: false)
        self.viewDidLayoutSubviews()
    }
}

extension UIImage {
    static func from(color: UIColor) -> UIImage {
        let rect = CGRect(x: 0, y: 0, width: 1, height: 1)
        UIGraphicsBeginImageContext(rect.size)
        let context = UIGraphicsGetCurrentContext()
        context!.setFillColor(color.cgColor)
        context!.fill(rect)
        let img = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return img!
    }
}
K. Janjuha
  • 167
  • 2
  • 11
2

I figured out a way to do this, by adding a separate shadow layer for the tab bar:

    tabBar.clipsToBounds = true
    tabBar.layer.cornerRadius = Siz_TabBar_CornerRadius

    // For some reason you have to use the vertically opposite corners for this to show up on iPad
    tabBar.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]

    shadowLayer = CALayer()
    shadowLayer.frame = tabBar.frame
    shadowLayer.backgroundColor = UIColor.clear.cgColor
    shadowLayer.cornerRadius = yourTabBarCornerRadius
    shadowLayer.shadowColor = yourShadowColor.cgColor
    shadowLayer.shadowRadius = yourShadowRadius
    shadowLayer.shadowOpacity = 1.0
    shadowLayer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]

    // This is important so the shadow doesn't lag content
    // which is scrolling underneath it.  You should tell the tab
    // bar layer to rasterize as well, the rounded corners can cause
    // performance issues with animated content underneath them.
    shadowLayer.shouldRasterize = true
    shadowLayer.rasterizationScale = UIScreen.main.scale

    // The shadow path is needed because a shadow won't
    // display for a layer with a clear backgroundColor
    let shadowPath = UIBezierPath(roundedRect: shadowLayer.bounds, byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: yourTabBarCornerRadius, height: yourTabBarCornerRadius))

    // The mask makes it so that the shadow doesn't draw on
    // top of the tab bar, filling in the whole layer
    let maskLayer = CAShapeLayer()
    let maskPath = CGMutablePath()

    // This path goes around the outside of the possible shadow radius, so that the
    // shadow is between this path and the tap bar
    maskPath.addRect(CGRect(x: -yourShadowRadius, y: -yourShadowRadius, width: shadowLayer.frame.width + yourShadowRadius, height: shadowLayer.frame.height + yourShadowRadius))

    // The shadow path (shape of the tab bar) is drawn on the inside
    maskPath.addPath(shadowPath.cgPath)
    maskLayer.path = maskPath

    // This makes it so that the only shadow layer content that will
    // be drawn is located in between the two above paths
    maskLayer.fillRule = .evenOdd
    shadowLayer.mask = maskLayer

    // View here is the tab bar controller's view
    view.layer.addSublayer(shadowLayer)
DivideByZer0
  • 745
  • 8
  • 26
2

You are right that setting masksToBounds=true is the only way to allow tabBar.layer.cornerRadius value to apply, but setting it to true will remove any shadows!

A lot of solutions involve adding a dummy view behind the tab bar, and apply shadows to it. It works but once I got it to work by adding the dummy view as a subview of the parent view, behind the tab bar, when I tried to navigate to another screen by pushing a view controller with hidesBottomBarWhenPushed=true, the tab bar is hidden but the dummy view remains on the screen :(

After 2 days of trial and error, I found a solution that doesn't require adding a dummy view, but instead it adds a sublayer to round the corners and set the drop shadows. It's simple enough that there's no subview involved and doesn't require masksToBounds to be set to true.

Inspired by this answer: https://stackoverflow.com/a/63793084/1241783

Instead of subclassing the UITabBar, I made a subclass of UITabBarController, and added this function:

private func addShape() {
    let shapeLayer = CAShapeLayer()
    shapeLayer.path = UIBezierPath(
        roundedRect: tabBar.bounds,
        byRoundingCorners: [.topLeft, .topRight],
        cornerRadii: CGSize(width: tabBarCornerRadius, height: 0.0)).cgPath
    shapeLayer.fillColor = UIColor.white.cgColor
    shapeLayer.shadowPath =  UIBezierPath(roundedRect: tabBar.bounds, cornerRadius: tabBarCornerRadius).cgPath
    shapeLayer.shadowColor = UIColor(red: 0, green: 0, blue: 0, alpha: 0.2).cgColor
    shapeLayer.shadowOpacity = 1
    shapeLayer.shadowRadius = 16
    shapeLayer.shadowOffset = CGSize(width: 0, height: -6)

    // To improve rounded corner and shadow performance tremendously
    shapeLayer.shouldRasterize = true
    shapeLayer.rasterizationScale = UIScreen.main.scale

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

Call this function in override of viewDidLayoutSubviews:

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    addShape()
}

And finally, the last piece of the puzzle, Tab bar automatically creates a backdrop view which doesn't have rounded corners, so we need to make the backdrop view transparent, by adding these two lines in viewDidLoad:

override func viewDidLoad() {
    super.viewDidLoad()
    tabBar.shadowImage = UIImage() // this removes the top line of the tabBar
    tabBar.backgroundImage = UIImage() // this changes the UI backdrop view of tabBar to transparent
}
Dharman
  • 30,962
  • 25
  • 85
  • 135
Bruce
  • 2,357
  • 5
  • 29
  • 50