Search code examples
iosswiftcalayermaskuibezierpath

Add subview on a parent drawn through UIBezierPath


I've a custom UITabBar. Its bar has a simple but customised shape: its height is bigger then default one, has rounded corners and (important) a shadow layer on the top. The result is this: CustomTabBar

Now I've to add an element that shows the selected section on the top of the bar, to achieve this: The goal

The problem is that no matter the way I choose to add this element (add a subview to the bar or add a new sublayer) but the new element will always be drawn outside the corners. I suppose this is because I can't enable the clipping mask (if I enable the clipping mask I'll kill the shadow and also, more important, the bezierpath) enter image description here

Do you have any tips for this? Basically, the goal should be:

have an element that moves horizontally (animated) but cannot be drawn outside the parent (the tabbar)

Actually, the code to draw the custom tabBar is:

class CustomTabBar: UITabBar {

    /// The layer that defines the custom shape
    private var shapeLayer: CALayer?
    /// The radius for the border of the bar
    var borderRadius: CGFloat = 0

    override func layoutSubviews() {
        super.layoutSubviews()

        // aspect and shadow
        isTranslucent       = false
        backgroundColor     = UIColor.white
        tintColor           = AZTheme.PaletteColor.primaryColor
        shadowImage         = nil
        layer.masksToBounds = false
        layer.shadowColor   = UIColor.black.cgColor
        layer.shadowOpacity = 0.1
        layer.shadowOffset  = CGSize(width: 0, height: -1)
        layer.shadowRadius  = 10
    }

    override func draw(_ rect: CGRect) {
        drawShape()
    }

    /// Draw and apply the custom shape to the bar
    func drawShape() {
        let shapeLayer = CAShapeLayer()
        shapeLayer.path = createPath()
        shapeLayer.fillColor = AZTheme.tabBarControllerBackgroundColor.cgColor

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

        self.shapeLayer = shapeLayer
    }
}

// MARK: - Private functions
extension CustomTabBar {
    /// Return the custom shape for the bar
    internal func createPath() -> CGPath {
        let height: CGFloat = self.frame.height
        let path = UIBezierPath()

        path.move(to: CGPoint(x: 0, y: 0))
        path.addArc(withCenter: CGPoint(x: borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi, endAngle: CGFloat.pi * (3/2), clockwise: true)
        path.addLine(to: CGPoint(x: frame.width - borderRadius, y: -borderRadius))
        path.addArc(withCenter: CGPoint(x: frame.width - borderRadius, y: 0), radius: borderRadius, startAngle: CGFloat.pi * (3/2), endAngle: 0, clockwise: true)
        path.addLine(to: CGPoint(x: frame.width, y: height))
        path.addLine(to: CGPoint(x: 0, y: height))
        path.close()

        return path.cgPath
    }
}

Solution

  • I solved splitting the owner of custom shape from the owner of the shadow in 2 different views. So I'm using 3 views achieve the goal.

    CustomTabBar: has default size and casts shadow with offset.
    |
    └ SelectorContainer: is a view with custom shape (BezierPath) that is
      positioned on the top of the TabBar to graphically "extend" the view
      and have the feeling of a bigger TabBar. It has rounded corners on
      the top-right, top-left margin. MaskToBounds enabled.
      |
      └ Selector: simple view that change the its origin through animation.
    

    See the result here

    The code:

    class CustomTabBar: UITabBar {
    
        /// The corner radius value for the top-left, top-right corners of the TabBar
        var borderRadius: CGFloat = 0
    
        /// Who is containing the selector. Is a subview of the TabBar.
        private var selectorParent: UIView?
        /// Who moves itself following the current section. Is a subview of ```selectorParent```.
        private var selector: UIView?
        /// The height of the ```selector```
        private var selectorHeight: CGFloat = 5
        /// The number of sections handled by the TabBarController.
        private var numberOfSections: Int = 0
    
        override func layoutSubviews() {
            super.layoutSubviews()
            isTranslucent       = false
            backgroundColor     = UIColor.white
            tintColor           = AZTheme.PaletteColor.primaryColor
            shadowImage         = nil
            layer.masksToBounds = false
            layer.shadowColor   = UIColor.black.cgColor
            layer.shadowOpacity = 0.1
            layer.shadowOffset  = CGSize(width: 0, height: -1)
            layer.shadowRadius  = 10
        }
    }
    
    // MARK: - Private functions
    extension CustomTabBar {
        /// Create the selector element on the top of the TabBar
        func setupSelectorView(numberOfSections: Int) {
            self.numberOfSections = numberOfSections
    
            // delete previous subviews (if exist)
            if let selectorContainer = self.selectorParent {
                selectorContainer.removeFromSuperview()
                self.selector?.removeFromSuperview()
                self.selectorParent = nil
                self.selector = nil
            }
    
            // SELECTOR CONTAINER
            let selectorContainerRect: CGRect = CGRect(x: 0,
                                                       y: -borderRadius,
                                                       width: frame.width,
                                                       height: borderRadius)
            let selectorContainer = UIView(frame: selectorContainerRect)
            selectorContainer.backgroundColor = UIColor.white
            selectorContainer.AZ_roundCorners([.topLeft, .topRight], radius: borderRadius)
            selectorContainer.layer.masksToBounds = true
            self.addSubview(selectorContainer)
    
            // SELECTOR
            let selectorRect: CGRect = CGRect(x: 0,
                                              y: 0,
                                              width: selectorContainer.frame.width / CGFloat(numberOfSections),
                                              height: selectorHeight)
            let selector = UIView(frame: selectorRect)
            selector.backgroundColor = AZTheme.PaletteColor.primaryColor
            selectorContainer.addSubview(selector)
    
            // add views to hierarchy
            self.selectorParent = selectorContainer
            self.selector = selector
        }
    
        /// Animate the position of the selector passing the index of the new section
        func animateSelectorTo(sectionIndex: Int) {
            guard let selectorContainer = self.selectorParent, let selector = self.selector else { return }
            selector.layer.removeAllAnimations()
    
            let sectionWidth: CGFloat = selectorContainer.frame.width / CGFloat(numberOfSections)
            let newCoord = CGPoint(x: sectionWidth * CGFloat(sectionIndex), y: selector.frame.origin.y)
            UIView.animate(withDuration: 0.25, delay: 0, options: UIView.AnimationOptions.curveEaseOut, animations: { [weak self] in
                self?.selector?.frame.origin = newCoord
            }, completion: nil)
        }
    }