swiftpathuitabbarcontrollercore-animationcclayer

curve in tabbar view and bottom popup not working?


Hello all, I tried to add arc for UIBezierPath I could not able to get the exact curve,

I Need like this design

here is my code here I have added the bezier path for the added curve from the center position.

@IBDesignable
class MyTabBar: 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
        
        //The below 4 lines are for shadow above the bar. you can skip them if you do not want a shadow
        shapeLayer.shadowOffset = CGSize(width:0, height:0)
        shapeLayer.shadowRadius = 10
        shapeLayer.shadowColor = UIColor.gray.cgColor
        shapeLayer.shadowOpacity = 0.3
        
        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
        
        path.addCurve(to: CGPoint(x: centerWidth, y: height),
                      controlPoint1: CGPoint(x: (centerWidth - 30), y: 0), controlPoint2: CGPoint(x: centerWidth - 35, y: height))
        
        path.addCurve(to: CGPoint(x: (centerWidth + height * 2), y: 0),
                      controlPoint1: CGPoint(x: centerWidth + 35, y: height), controlPoint2: CGPoint(x: (centerWidth + 30), y: 0))
        
        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 hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        guard !clipsToBounds && !isHidden && alpha > 0 else { return nil }
        for member in subviews.reversed() {
            let subPoint = member.convert(point, from: self)
            guard let result = member.hitTest(subPoint, with: event) else { continue }
            return result
        }
        return nil
    }
}

this is tab bar controller added plus button in center view center, and the when tap the plus button to add the curve based popup should show, I don't know how to add curve based popup.

class TabbarViewController: UITabBarController,UITabBarControllerDelegate {

    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
        self.navigationController?.navigationBar.isHidden = true
        setupMiddleButton()
    }
    // TabBarButton – Setup Middle Button
    func setupMiddleButton() {
        let middleBtn = UIButton(frame: CGRect(x: (self.view.bounds.width / 2)-25, y: -20, width: 50, height: 50))
        middleBtn.setImage(UIImage(named: "PlusBtn"), for: .normal)
        self.tabBar.addSubview(middleBtn)
        middleBtn.addTarget(self, action: #selector(self.menuButtonAction), for: .touchUpInside)
        self.view.layoutIfNeeded()
    }
    // Menu Button Touch Action
    @objc func menuButtonAction(sender: UIButton) {
        //show the popUp
    }
}

Please share me the findings & share me your refreance Thanks


Solution

  • New edit:

    I created a general-purpose method that will generate polygons with a mixture of sharp and rounded corners of different radii. I used it to create a project with a look rather like what you are after. You can download it from Github:

    https://github.com/DuncanMC/CustomTabBarController.git

    Here's what it looks like:

    enter image description here

    Note that the area of the tab's view controller that extends into the tab bar controller does not get taps. If you try to tap there, it triggers the tab bar controller. You will have to do some more tinkering to get that part to work.

    Ultimately you may have to create a custom UITabBar (or UITabBar-like component) and possibly a custom parent view controller that acts like a UITabBar, in order to get what you want.

    The method that creates polygon paths is called buildPolygonPathFrom(points:defaultCornerRadius:)

    It takes an array of PolygonPoint structs. Those are defined like this:

    /// A struct describing a single vertex in a polygon. Used in building polygon paths with a mixture of rounded an sharp-edged vertexes.
    public struct PolygonPoint {
        let point: CGPoint
        let isRounded: Bool
        let customCornerRadius: CGFloat?
        init(point: CGPoint, isRounded: Bool, customCornerRadius: CGFloat? = nil) {
            self.point = point
            self.isRounded = isRounded
            self.customCornerRadius = customCornerRadius
        }
    
        init(previousPoint: PolygonPoint, isRounded: Bool) {
            self.init(point: previousPoint.point, isRounded: isRounded, customCornerRadius: previousPoint.customCornerRadius)
        }
    }
    

    The code to build the path for the custom tab bar looks like this:

    func tabBarMaskPath() -> CGPath? {
        let width = bounds.width
        let height = bounds.height
        guard width > 0 && height > 0 else { return nil }
        let dentRadius: CGFloat = 35
        let cornerRadius: CGFloat = 20
        let topFlatPartWidth = (width - dentRadius * 2.0) / 2
        let polygonPoints = [
            PolygonPoint(point: CGPoint(x:  0, y:  0),                                                          // Point 0
                         isRounded: true,
                         customCornerRadius: cornerRadius),
            PolygonPoint(point: CGPoint(x:  0, y:  height),                                                     // Point 1
                         isRounded: false),
            PolygonPoint(point: CGPoint(x:  width, y:  height),                                                 // Point 2
                         isRounded: false),
            PolygonPoint(point: CGPoint(x:  width, y:  0),                                                      // Point 3
                         isRounded: true,
                         customCornerRadius: cornerRadius),
            PolygonPoint(point: CGPoint(x:  topFlatPartWidth + dentRadius * 2, y:  0),                          // Point 4
                         isRounded: true,
                         customCornerRadius: cornerRadius),
            PolygonPoint(point: CGPoint(x:  topFlatPartWidth + dentRadius * 2, y:  dentRadius + cornerRadius),  // Point 5
                         isRounded: true,
                         customCornerRadius: dentRadius),
            PolygonPoint(point: CGPoint(x:  topFlatPartWidth , y:  dentRadius  + cornerRadius),                  // Point 6
                         isRounded: true,
                         customCornerRadius: dentRadius),
            PolygonPoint(point: CGPoint(x:  topFlatPartWidth , y:  0),                                           // Point 7
                         isRounded: true,
                         customCornerRadius: cornerRadius),
        ]
        return buildPolygonPathFrom(points: polygonPoints, defaultCornerRadius: 0)
    }
    

    Previous answer:

    I just tried it, and it is possible to subclass UITabBar. I created a subclass of UITabBar where I use a mask layer to cut a circular "notch" out of the top of the tab bar. The code is below. It looks like the screenshot below. It isn't quite what you're after, but it's a start:

    (The background color for the "Page 1" view controller is set to light gray, and you can see that color showing through in the "notch" I cut out of the tab bar.)

    enter image description here

    //
    //  CustomTabBar.swift
    //  TabBarController
    //
    //  Created by Duncan Champney on 3/31/21.
    //
    
    import UIKit
    
    class CustomTabBar: UITabBar {
    
        var maskLayer = CAShapeLayer()
    
        override var frame: CGRect {
            didSet {
                configureMaskLayer()
            }
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            configureMaskLayer()
            self.layer.mask = maskLayer
            self.layer.borderWidth = 0
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            configureMaskLayer()
            self.layer.mask = maskLayer
        }
    
        func     configureMaskLayer() {
            let rect = layer.bounds
            maskLayer.frame = rect
            let circleBoxSize = rect.size.height * 1.25
            maskLayer.fillRule = .evenOdd
            let path = UIBezierPath(rect: rect)
            let circleRect = CGRect(x: rect.size.width/2 - circleBoxSize / 2,
                                    y: -circleBoxSize/2,
                                    width: circleBoxSize,
                                    height: circleBoxSize)
            let circle = UIBezierPath.init(ovalIn: circleRect)
            path.append(circle)
            maskLayer.path = path.cgPath
            maskLayer.fillColor = UIColor.white.cgColor // Any opaque color works and has no effect
        }
    }
    

    Edit:

    To draw your popover view you'll need to create a filled path that shape. You'll have to construct a custom shape like that with a combination of lines and arcs. I suggest using a CGMutablePath and the method addArc(tangent1End:tangent2End:radius:transform:) since that enables you to provide endpoints rather than angles.

    Edit #2:

    Another part of the puzzle:

    Here is a custom UIView subclass that masks itself in the shape you're after

    //
    //  ShapeWithTabView.swift
    //  ShapeWithTab
    //
    //  Created by Duncan Champney on 4/1/21.
    //
    
    import UIKit
    
    class ShapeWithTabView: UIView {
    
        var cornerRadius: CGFloat = 20
        var tabRadius: CGFloat = 60
        var tabExtent: CGFloat = 0
    
        var shapeLayer = CAShapeLayer()
        var maskLayer = CAShapeLayer()
    
        func buildShapeLayerPath() -> CGPath {
            let boxWidth = min(bounds.size.width - 40, 686)
            let boxHeight = min(bounds.size.height - 40 - tabRadius * 2 - tabExtent, 832)
    
            // These are the corners of the view's primary rectangle
            let point1 = CGPoint(x: 0, y: boxHeight)
            let point2 = CGPoint(x: 0, y: 0)
            let point3 = CGPoint(x: boxWidth, y: 0)
            let point4 = CGPoint(x: boxWidth, y: boxHeight)
    
            // These are the corners of the "tab" that extends outside the view's normal bounds.
            let tabPoint1 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight)
            let tabPoint2 = CGPoint(x: boxWidth / 2 + tabRadius, y: boxHeight + tabExtent + tabRadius * 2 )
            let tabPoint3 = CGPoint(x: boxWidth / 2 - tabRadius, y: boxHeight + tabExtent + tabRadius * 2)
            let tabPoint4 = CGPoint(x: boxWidth / 2 - tabRadius , y: boxHeight)
    
            let path = CGMutablePath()
            path.move(to: CGPoint(x: 0, y: boxHeight - cornerRadius))
            path.addArc(tangent1End: point2,
                        tangent2End: point3,
                        radius: cornerRadius)
            path.addArc(tangent1End: point3,
                        tangent2End: point4,
                        radius: cornerRadius)
            path.addArc(tangent1End: point4,
                        tangent2End: point1,
                        radius: cornerRadius)
    //
            path.addArc(tangent1End: tabPoint1,
                        tangent2End: tabPoint2,
                        radius: tabRadius)
            path.addArc(tangent1End: tabPoint2,
                        tangent2End: tabPoint3,
                        radius: tabRadius)
            path.addArc(tangent1End: tabPoint3,
                        tangent2End: tabPoint4,
                        radius: tabRadius)
            path.addArc(tangent1End: tabPoint4,
                        tangent2End: point1,
                        radius: tabRadius)
    
            path.addArc(tangent1End: point1,
                        tangent2End: point2,
                        radius: cornerRadius)
    
            return path
        }
    
        func doInitSetup() {
            self.layer.addSublayer(shapeLayer)
            self.layer.mask = maskLayer
            backgroundColor = .lightGray
    
            //Configure a shape layer to draw an outline
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.strokeColor = UIColor.blue.cgColor
            shapeLayer.lineWidth = 2
    
            //Configure a mask layer to mask the view to our custom shape
            maskLayer.fillColor = UIColor.white.cgColor
            maskLayer.strokeColor = UIColor.white.cgColor
            maskLayer.lineWidth = 2
        }
    
        override init(frame: CGRect) {
            super.init(frame: frame)
            self.doInitSetup()
        }
    
        required init?(coder: NSCoder) {
            super.init(coder: coder)
            self.doInitSetup()
        }
        public func updateShapeLayerPath() {
            let path = buildShapeLayerPath()
            shapeLayer.path =  path
            maskLayer.path = path
        }
    
        override var frame: CGRect {
            didSet {
                print("New frame = \(frame)")
                shapeLayer.frame = layer.bounds
            }
        }
    }
    

    Combined with the modified tab bar from above, it looks like the image below. The final task is to get the custom view sized and positioned correctly, and have it land on top of the tab bar.

    enter image description here